From 4c91880a858c1936985e5b08af4160f8278db36e Mon Sep 17 00:00:00 2001 From: Sven Van de Velde Date: Sun, 25 Mar 2018 20:27:18 +0200 Subject: [PATCH 001/147] Improved dynamic loader process --- .../Moose/{Moose.lua => Globals.lua} | 0 Moose Setup/Moose_Create.lua | 19 +++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) rename Moose Development/Moose/{Moose.lua => Globals.lua} (100%) diff --git a/Moose Development/Moose/Moose.lua b/Moose Development/Moose/Globals.lua similarity index 100% rename from Moose Development/Moose/Moose.lua rename to Moose Development/Moose/Globals.lua diff --git a/Moose Setup/Moose_Create.lua b/Moose Setup/Moose_Create.lua index 500a56965..5bfd2ed6d 100644 --- a/Moose Setup/Moose_Create.lua +++ b/Moose Setup/Moose_Create.lua @@ -17,10 +17,10 @@ local MooseFilePath = MooseTargetPath.."/Moose.lua" print( "Reading Moose source list : " .. MooseSourcesFilePath ) -local MooseFile = io.open( MooseFilePath, "w" ) +local LoaderFile = io.open( MooseFilePath, "w" ) if MooseDynamicStatic == "S" then - MooseFile:write( "env.info( '*** MOOSE GITHUB Commit Hash ID: " .. MooseCommitHash .. " ***' )\n" ) + LoaderFile:write( "env.info( '*** MOOSE GITHUB Commit Hash ID: " .. MooseCommitHash .. " ***' )\n" ) end local MooseLoaderPath @@ -31,12 +31,14 @@ if MooseDynamicStatic == "S" then MooseLoaderPath = MooseSetupPath .. "/Moose Templates/Moose_Static_Loader.lua" end +local MooseFile = io.open( MooseFilePath, "w" ) + local MooseLoader = io.open( MooseLoaderPath, "r" ) local MooseLoaderText = MooseLoader:read( "*a" ) MooseLoader:close() -MooseFile:write( MooseLoaderText ) - +LoaderFile:write( MooseLoaderText ) +LoaderFile:write( "__Moose.Include( 'Scripts/Moose/Moose.lua'\n" ) local MooseSourcesFile = io.open( MooseSourcesFilePath, "r" ) local MooseSource = MooseSourcesFile:read("*l") @@ -55,7 +57,7 @@ while( MooseSource ) do local MooseSourceFileText = MooseSourceFile:read( "*a" ) MooseSourceFile:close() - MooseFile:write( MooseSourceFileText ) + LoaderFile:write( MooseSourceFileText ) end end @@ -63,13 +65,14 @@ while( MooseSource ) do end if MooseDynamicStatic == "D" then - MooseFile:write( "BASE:TraceOnOff( true )\n" ) + LoaderFile:write( "BASE:TraceOnOff( true )\n" ) end if MooseDynamicStatic == "S" then - MooseFile:write( "BASE:TraceOnOff( false )\n" ) + LoaderFile:write( "BASE:TraceOnOff( false )\n" ) end -MooseFile:write( "env.info( '*** MOOSE INCLUDE END *** ' )\n" ) +LoaderFile:write( "env.info( '*** MOOSE INCLUDE END *** ' )\n" ) MooseSourcesFile:close() +LoaderFile:close() MooseFile:close() From 856bd56cdef97ebd15cd7ac428e2b5b5b210c169 Mon Sep 17 00:00:00 2001 From: Sven Van de Velde Date: Sun, 25 Mar 2018 20:33:43 +0200 Subject: [PATCH 002/147] Improved dynamic loading --- Moose Setup/Moose.files | 2 +- Moose Setup/Moose_Create.lua | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 1e575f0f2..4459578ec 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -79,4 +79,4 @@ Tasking/Task_A2A.lua Tasking/Task_Cargo.lua Tasking/TaskZoneCapture.lua -Moose.lua +Globals.lua diff --git a/Moose Setup/Moose_Create.lua b/Moose Setup/Moose_Create.lua index 5bfd2ed6d..5a15f59ef 100644 --- a/Moose Setup/Moose_Create.lua +++ b/Moose Setup/Moose_Create.lua @@ -13,11 +13,12 @@ print( "Moose setup path : " .. MooseSetupPath ) print( "Moose target path : " .. MooseTargetPath ) local MooseSourcesFilePath = MooseSetupPath .. "/Moose.files" -local MooseFilePath = MooseTargetPath.."/Moose.lua" +local LoaderFilePath = MooseTargetPath.."/Moose.lua" +local MooseFilePath = MooseDevelopmentPath .. "/Moose.lua" print( "Reading Moose source list : " .. MooseSourcesFilePath ) -local LoaderFile = io.open( MooseFilePath, "w" ) +local LoaderFile = io.open( LoaderFilePath, "w" ) if MooseDynamicStatic == "S" then LoaderFile:write( "env.info( '*** MOOSE GITHUB Commit Hash ID: " .. MooseCommitHash .. " ***' )\n" ) From 74d2ee94307af6b8611c635f1e255da9b05b30ed Mon Sep 17 00:00:00 2001 From: Sven Van de Velde Date: Sun, 25 Mar 2018 20:49:49 +0200 Subject: [PATCH 003/147] Improvements --- Moose Setup/Moose_Create.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Setup/Moose_Create.lua b/Moose Setup/Moose_Create.lua index 5a15f59ef..b076b8d87 100644 --- a/Moose Setup/Moose_Create.lua +++ b/Moose Setup/Moose_Create.lua @@ -14,7 +14,7 @@ print( "Moose target path : " .. MooseTargetPath ) local MooseSourcesFilePath = MooseSetupPath .. "/Moose.files" local LoaderFilePath = MooseTargetPath.."/Moose.lua" -local MooseFilePath = MooseDevelopmentPath .. "/Moose.lua" +local MooseFilePath = MooseTargetPath .. "/Loader.lua" print( "Reading Moose source list : " .. MooseSourcesFilePath ) From ae754fe334304874e077c805b25cf859f7d67729 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 29 Oct 2018 00:01:53 +0100 Subject: [PATCH 004/147] CT v0.1.2 --- .../Moose/Functional/CarrierTrainer.lua | 1938 +++++++++++++++++ 1 file changed, 1938 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..3506c662e --- /dev/null +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -0,0 +1,1938 @@ +--- **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. +-- +-- === +-- +-- ### Authors: **Bankler** (original idea and script), **funkyfranky** (MOOSE class implementation and enhancements) +-- +-- @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" +} + +--- LSO calls. +-- @type CARRIERTRAINER.LSOcall +-- @field Core.UserSound#USERSOUND RIGHTFORLINEUPL "Right for line up!" call (loud). +-- @field Core.UserSound#USERSOUND RIGHTFORLINEUPS "Right for line up." call. +-- @field #string RIGHTFORLINEUPT "Right for line up" text. +-- @field Core.UserSound#USERSOUND COMELEFTL "Come left!" call (loud). +-- @field Core.UserSound#USERSOUND COMELEFTS "Come left." call. +-- @field #string COMELEFTT "Come left" text. +-- @field Core.UserSound#USERSOUND HIGHL "You're high!" call (loud). +-- @field Core.UserSound#USERSOUND HIGHS "You're high." call. +-- @field #string HIGHT "You're high" text. +-- @field Core.UserSound#USERSOUND POWERL "Power!" call (loud). +-- @field Core.UserSound#USERSOUND POWERS "Power." call. +-- @field #string POWERT "Power" text. +-- @field Core.UserSound#USERSOUND CALLTHEBALL "Call the ball." call. +-- @field #string CALLTHEBALLT "Call the ball." text. +-- @field Core.UserSound#USERSOUND ROGERBALL "Roger, ball." call. +-- @field #string ROGERBALLT "Roger, ball." text. +-- @field Core.UserSound#USERSOUND WAVEOFF "Wave off!" call. +-- @field #string WAVEOFFT "Wave off!" text. +-- @field Core.UserSound#USERSOUND BOLTER "Bolter, bolter!" call. +-- @field #string BOLTERT "Bolter, bolter!" text. +-- @field Core.UserSound#USERSOUND LONGGROOVE "You're long in the groove. Depart and re-enter." call. +-- @field #string LONGGROOVET "You're long in the groove. Depart and re-enter." text. +CARRIERTRAINER.LSOcall={ + RIGHTFORLINEUPL=USERSOUND:New("LSO - RightLineUp(L).ogg"), + RIGHTFORLINEUPS=USERSOUND:New("LSO - RightLineUp(S).ogg"), + RIGHTFORLINEUPT="Right for line up", + COMELEFTL=USERSOUND:New("LSO - ComeLeft(L).ogg"), + COMELEFTS=USERSOUND:New("LSO - ComeLeft(S).ogg"), + COMELEFTT="Come left", + HIGHL=USERSOUND:New("LSO - High(L).ogg"), + HIGHS=USERSOUND:New("LSO - High(S).ogg"), + HIGHT="You're high", + POWERL=USERSOUND:New("LSO - Power(L).ogg"), + POWERS=USERSOUND:New("LSO - Power(S).ogg"), + POWERT="Power", + CALLTHEBALL=USERSOUND:New("LSO - Call the Ball.ogg"), + CALLTHEBALLT="Call the ball.", + ROGERBALL=USERSOUND:New("LSO - Roger.ogg"), + ROGERBALLT="Roger ball!", + WAVEOFF=USERSOUND:New("LSO - WaveOff.ogg"), + WAVEOFFT="Wave off!", + BOLTER=USERSOUND:New("LSO - Bolter.ogg"), + BOLTERT="Bolter, Bolter!", + LONGGROOVE=USERSOUND:New("LSO - Long in Groove.ogg"), + LONGGROOVET="You're lon in the groove. Depart and re-enter.", +} + +--- 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 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 waveoff If true, player was waved off. +-- @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.2" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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 + + CARRIERTRAINER.LSOcall.HIGHL:ToGroup(_group) + + 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() + + -- We did land. + playerData.landed=true + + --TODO: maybe check that we actually landed on the right carrier. + + -- 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.longDownwindDone=false + playerData.boltered=false + playerData.landed=false + playerData.waveoff=false + playerData.calledball=false + playerData.Tlso=timer.getTime() + return playerData +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) + + -- Get relative heading. + local relhead=self:_GetRelativeHeading(playerData.unit) + + -- One NM from carrier is way too far. + local limit = -UTILS.NMToMeters(1) + + local text=string.format("Long groove check: diffX=%d, relhead=%.1f", diffX, relhead) + self:T(text) + --MESSAGE:New(text, 1):ToAllIf(self.Debug) + + -- Check we are not too far out w.r.t back of the boat. + if 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 + + -- Time since last LSO call. + local time=timer.getTime() + local deltaT=time-playerData.Tlso + + -- Player group. + local player=playerData.unit:GetGroup() + + -- Check if we are beween 3/4 NM and end of ship. + if diffX>-UTILS.NMToMeters(0.75) and diffX<-100 and deltaT>=3 then + + local text="" + + -- Glideslope high/low calls. + if glideslopeError>1 then + text="You're too high! Throttles back!" + CARRIERTRAINER.LSOcall.HIGHL:ToGroup(player) + elseif glideslopeError>0.5 then + text="You're slightly high. Decrease power." + CARRIERTRAINER.LSOcall.HIGHS:ToGroup(player) + elseif glideslopeError<1.0 then + text="Power! You're way too low." + CARRIERTRAINER.LSOcall.POWERL:ToGroup(player) + elseif glideslopeError<0.5 then + text="You're slightly low. Increase power." + CARRIERTRAINER.LSOcall.POWERS:ToGroup(player) + else + text="Good altitude." + end + + -- Lineup left/right calls. + if lineuperror>3 then + text=text.."Come left!" + CARRIERTRAINER.LSOcall.COMELEFTL:ToGroup(player) + elseif lineuperror>1 then + text=text.."Come left." + CARRIERTRAINER.LSOcall.COMELEFTS:ToGroup(player) + elseif lineuperror<3 then + text=text.."Right for lineup!" + CARRIERTRAINER.LSOcall.RIGHTFORLINEUPL:ToGroup(player) + elseif lineuperror<1 then + text=text.."Right for lineup." + CARRIERTRAINER.LSOcall.RIGHTFORLINEUPS:ToGroup(player) + else + text=text.."Good lineup." + end + + -- LSO Message to player. + self:_SendMessageToPlayer(text, 8, playerData, true) + + -- Set last time. + playerData.Tlso=time + + elseif diffX > 150 then + + local wire = 0 + local hint = "" + local score = 0 + if playerData.landed then + hint = "You boltered." + else + hint = "You were waved off." + wire = -1 + score = -10 + end + + -- Send message to player. + self:_SendMessageToPlayer(hint, 8, playerData) + + -- Add to debrief. + self:_AddToSummary(playerData, "Calling the Ball", hint) + + -- Next step: debrief. + playerData.step = 99 + 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 + + +--------- +-- Bla functions +--------- + +--- 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) + table.insert(playerData.debrief, {step=step, hint=item}) +end + +--- Show debriefing message. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +function CARRIERTRAINER:_Debrief(playerData) + + -- Debriefing text. + local text=string.format("Debriefing:\n") + text=text..string.format("===========\n\n") + for _,_data in pairs(playerData.debrief) do + local step=_data.step + local comment=_data.hint + text=text..string.format("* %s:\n",step) + text=text..string.format("- %s\n", comment) + text=text..string.format("------------------------------------\n\n") + end + + -- Send debrief message to player + self:_SendMessageToPlayer(text, 60, playerData, true) + + --TODO: add final grades, memorize score deductions. + --self:_PrintFinalScore(playerData, 30, -2) + --self:_HandleCollectedResult(playerData, -2) + + -- Next step. + playerData.step=0 +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 playerData.longDownwindDone==false 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) + + -- Text where we are wrong. + local toofartext=self:_TooFarOutText(X, Z, posData) + + -- Send message to player. + self:_SendMessageToPlayer(toofartext.." Abort approach!", 15, playerData, true) + + -- 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) + + -- Add to debrief. + self:_AddToSummary(playerData, "Abort", "Approach aborted.") + + --TODO: set score and grade. + + -- Next step debrief. + playerData.step=99 +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 + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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. +-- @param #boolean clear If true, clear screen from previous messages. +function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData, clear) + if playerData.client then + MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToClient(playerData.client) + 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 + + +--- 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 + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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 + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- LSO Class +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- LSO class. +-- @type LSO +-- @field #string ClassName Name of the class. +-- @extends Core.Fsm#FSM + +--- Landing Signal Officer +-- +-- === +-- +-- ![Banner Image](..\Presentations\LSO\LSO_Main.png) +-- +-- # The Landing Signal Officer +-- +-- bla bla +-- +-- @field #LSO +LSO = { + ClassName = "LSO", +} + From 5f40feb1affd4a570733340b96784aa021c250aa Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Mon, 29 Oct 2018 16:25:09 +0100 Subject: [PATCH 005/147] CTv0.1.2w --- Moose Development/Moose/Core/UserSound.lua | 12 +++- .../Moose/Functional/CarrierTrainer.lua | 67 ++++++++++++++----- 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/Moose Development/Moose/Core/UserSound.lua b/Moose Development/Moose/Core/UserSound.lua index a0547a5cf..b0f6fb393 100644 --- a/Moose Development/Moose/Core/UserSound.lua +++ b/Moose Development/Moose/Core/UserSound.lua @@ -118,15 +118,21 @@ do -- UserSound --- Play the usersound to the given @{Wrapper.Group}. -- @param #USERSOUND self -- @param Wrapper.Group#GROUP Group The @{Wrapper.Group} to play the usersound to. + -- @param #number Delay (Optional) Delay in seconds, before the sound is played. Default 0. -- @return #USERSOUND The usersound instance. -- @usage -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) -- local PlayerGroup = GROUP:FindByName( "PlayerGroup" ) -- Search for the active group named "PlayerGroup", that contains a human player. -- BlueVictory:ToGroup( PlayerGroup ) -- Play the sound that Blue has won to the player group. -- - function USERSOUND:ToGroup( Group ) --R2.3 - - trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName ) + function USERSOUND:ToGroup( Group, Delay ) --R2.3 + + Delay=Delay or 0 + if Delay>0 then + SCHEDULER:New(nil, USERSOUND.ToGroup,{self, Group}, Delay) + else + trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName ) + end return self end diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 3506c662e..49215a2f0 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -201,7 +201,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.2" +CARRIERTRAINER.version="0.1.2w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -384,6 +384,7 @@ function CARRIERTRAINER:OnEventBirth(EventData) self:_InitNewRound(self.players[_playername]) end + -- Test CARRIERTRAINER.LSOcall.HIGHL:ToGroup(_group) end @@ -839,6 +840,9 @@ function CARRIERTRAINER:_CallTheBall(playerData) -- Player altitude local alt=playerData.unit:GetAltitude() + + -- Player group. + local player=playerData.unit:GetGroup() -- Get velocities. local playerVelocity = playerData.unit:GetVelocityKMH() @@ -849,20 +853,55 @@ function CARRIERTRAINER:_CallTheBall(playerData) 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 + -- Runway is at an angle of -10 degrees wrt to carrier X direction. + -- TODO: make this carrier dependent + local rwyangle=-10 + local deckheight=22 + local tailpos=-100 + + -- Position at the end of the deck. From there we calculate the angle. + -- TODO: Check exact number and make carrier dependent. + local b={} + b.x=tailpos + b.z=0 + + -- Position of the aircraft wrt carrier coordinates. + local a={} + a.x=diffX + a.z=diffZ + + --a.x=-200 + --a.y= 0 + --a.z=17.632698070846 --(100)*math.tan(math.rad(10)) + --a.z=20 + --print(a.z) + + -- Vector from plane to ref point on boad. + local c={} + c.x=b.x-a.x + c.z=b.z-a.z + + -- Current line up and error wrt to final heading of the runway. + local lineup=math.atan2(c.z, c.x) + local lineuperror=math.deg(lineup)-rwyangle + + if lineuperror<0 then + env.info("come left") + elseif lineuperror>0 then + env.info("Right for lineup") + end + -- 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 h=playerData.unit:GetAltitude()-deckheight + local x=math.abs(diffX-tailpos) + local glideslope=math.atan(h/x) + 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 + CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(player) return end @@ -870,8 +909,6 @@ function CARRIERTRAINER:_CallTheBall(playerData) local time=timer.getTime() local deltaT=time-playerData.Tlso - -- Player group. - local player=playerData.unit:GetGroup() -- Check if we are beween 3/4 NM and end of ship. if diffX>-UTILS.NMToMeters(0.75) and diffX<-100 and deltaT>=3 then @@ -896,16 +933,16 @@ function CARRIERTRAINER:_CallTheBall(playerData) end -- Lineup left/right calls. - if lineuperror>3 then + if lineuperror<3 then text=text.."Come left!" CARRIERTRAINER.LSOcall.COMELEFTL:ToGroup(player) - elseif lineuperror>1 then + elseif lineuperror<1 then text=text.."Come left." CARRIERTRAINER.LSOcall.COMELEFTS:ToGroup(player) - elseif lineuperror<3 then + elseif lineuperror>3 then text=text.."Right for lineup!" CARRIERTRAINER.LSOcall.RIGHTFORLINEUPL:ToGroup(player) - elseif lineuperror<1 then + elseif lineuperror>1 then text=text.."Right for lineup." CARRIERTRAINER.LSOcall.RIGHTFORLINEUPS:ToGroup(player) else From dfd20ced59f8ce0bca7b3dd103c30c2d1ffd68f6 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 30 Oct 2018 00:02:45 +0100 Subject: [PATCH 006/147] CT v0.1.3 --- .../Moose/Functional/CarrierTrainer.lua | 290 +++++++++++------- 1 file changed, 175 insertions(+), 115 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 49215a2f0..5053691c5 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -201,7 +201,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.2w" +CARRIERTRAINER.version="0.1.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -386,6 +386,8 @@ function CARRIERTRAINER:OnEventBirth(EventData) -- Test CARRIERTRAINER.LSOcall.HIGHL:ToGroup(_group) + CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(_group, 10) + MESSAGE:New(CARRIERTRAINER.LSOcall.HIGHT, 5):ToAllIf(self.Debug) end end @@ -627,16 +629,18 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) CARRIERTRAINER.LSOcall.LONGGROOVE:ToGroup(playerData.unit:GetGroup()) -- Debrief. - self:_AddToSummary(playerData, "Long Downwind Leg", hint) + self:_AddToSummary(playerData, "Long in the groove", hint) -- Decrease score. playerData.score=playerData.score-40 + local grade="LIG PATTERN WAVE OFF - CUT 1 PT" + -- Long downwind done! playerData.longDownwindDone = true -- Next step: Debriefing. - playerData.step=99 + playerData.step=999 end @@ -735,6 +739,10 @@ function CARRIERTRAINER:_Ninety(playerData) -- Next step: wake. playerData.step = 7 + + elseif relheading>90 and self:_CheckLimits(diffX, diffZ, self.Wake) then + -- Message to player. + self:_SendMessageToPlayer("You are already at the wake and have not passed the 90! Turn faster next time!", 10, playerData) end end @@ -784,31 +792,28 @@ end 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) + local diffX, diffZ, rho, phi = 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 calltheball=UTILS.NMToMeters(0.75) + + if rho<=calltheball then local alt = playerData.unit:GetAltitude() local aoa = playerData.unit:GetAoA() + self:_SendMessageToPlayer("Call the ball.", 8, playerData) + playerData.calledball=true + CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(playerData.unit:GetGroup()) + -- Grade altitude. local hintAlt=self:_AltitudeCheck(playerData, self.Groove, alt) @@ -822,14 +827,15 @@ function CARRIERTRAINER:_Groove(playerData) self:_SendMessageToPlayer(hintFull, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, "Entering the Groove", hintFull) + self:_AddToSummary(playerData, "Calling the ball", hintFull) -- Next step. - playerData.step = 9 + playerData.step = 90 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. @@ -842,119 +848,74 @@ function CARRIERTRAINER:_CallTheBall(playerData) local alt=playerData.unit:GetAltitude() -- Player group. - local player=playerData.unit:GetGroup() - - -- Get velocities. - local playerVelocity = playerData.unit:GetVelocityKMH() - local carrierVelocity = self.carrier:GetVelocityKMH() + local player=playerData.unit:GetGroup() -- Check abort conditions. if self:_CheckAbort(diffX, diffZ, self.Trap) then self:_AbortPattern(playerData, diffX, diffZ, self.Trap) return end - + -- Runway is at an angle of -10 degrees wrt to carrier X direction. -- TODO: make this carrier dependent local rwyangle=-10 local deckheight=22 - local tailpos=-100 - - -- Position at the end of the deck. From there we calculate the angle. - -- TODO: Check exact number and make carrier dependent. - local b={} - b.x=tailpos - b.z=0 - - -- Position of the aircraft wrt carrier coordinates. - local a={} - a.x=diffX - a.z=diffZ - - --a.x=-200 - --a.y= 0 - --a.z=17.632698070846 --(100)*math.tan(math.rad(10)) - --a.z=20 - --print(a.z) - - -- Vector from plane to ref point on boad. - local c={} - c.x=b.x-a.x - c.z=b.z-a.z - - -- Current line up and error wrt to final heading of the runway. - local lineup=math.atan2(c.z, c.x) - local lineuperror=math.deg(lineup)-rwyangle - - if lineuperror<0 then - env.info("come left") - elseif lineuperror>0 then - env.info("Right for lineup") - end + local tailpos=-100 + local lineup=self:_Lineup(playerData) + local lineupError=lineup-rwyangle + -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. local h=playerData.unit:GetAltitude()-deckheight local x=math.abs(diffX-tailpos) local glideslope=math.atan(h/x) - local glideslopeError=math.deg(glideslope) - 3.5 + 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 - CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(player) - return + -- Ranges in the groove. + local RRB=UTILS.NMToMeters(0.500) + local RIM=UTILS.NMToMeters(0.375) --0.75/2 + local RIC=UTILS.NMToMeters(0.100) + local RAR=UTILS.NMToMeters(0.050) + + if rho<=RRB and playerData.step==90 then + + -- Roger ball! + self:_SendMessageToPlayer(CARRIERTRAINER.LSOcall.ROGERBALLT, 8, playerData) + CARRIERTRAINER.LSOcall.ROGERBALL:ToGroup(player) + + playerData.step=91 + + elseif rho<=RIM and playerData.step==91 then + + --TODO: grade for IM + self:_SendMessageToPlayer("IM", 8, playerData) + + playerData.step=92 + + elseif rho<=RIM and playerData.step==92 then + + --TODO: grade for IC, call wave off? + self:_SendMessageToPlayer("IC", 8, playerData) + + playerData.step=93 + + elseif rho<=RAR and playerData.step==93 then + + --TODO: grade for AR + self:_SendMessageToPlayer("AR", 8, playerData) + + playerData.step=94 end -- Time since last LSO call. local time=timer.getTime() local deltaT=time-playerData.Tlso - -- Check if we are beween 3/4 NM and end of ship. - if diffX>-UTILS.NMToMeters(0.75) and diffX<-100 and deltaT>=3 then - - local text="" - - -- Glideslope high/low calls. - if glideslopeError>1 then - text="You're too high! Throttles back!" - CARRIERTRAINER.LSOcall.HIGHL:ToGroup(player) - elseif glideslopeError>0.5 then - text="You're slightly high. Decrease power." - CARRIERTRAINER.LSOcall.HIGHS:ToGroup(player) - elseif glideslopeError<1.0 then - text="Power! You're way too low." - CARRIERTRAINER.LSOcall.POWERL:ToGroup(player) - elseif glideslopeError<0.5 then - text="You're slightly low. Increase power." - CARRIERTRAINER.LSOcall.POWERS:ToGroup(player) - else - text="Good altitude." - end - - -- Lineup left/right calls. - if lineuperror<3 then - text=text.."Come left!" - CARRIERTRAINER.LSOcall.COMELEFTL:ToGroup(player) - elseif lineuperror<1 then - text=text.."Come left." - CARRIERTRAINER.LSOcall.COMELEFTS:ToGroup(player) - elseif lineuperror>3 then - text=text.."Right for lineup!" - CARRIERTRAINER.LSOcall.RIGHTFORLINEUPL:ToGroup(player) - elseif lineuperror>1 then - text=text.."Right for lineup." - CARRIERTRAINER.LSOcall.RIGHTFORLINEUPS:ToGroup(player) - else - text=text.."Good lineup." - end - - -- LSO Message to player. - self:_SendMessageToPlayer(text, 8, playerData, true) - - -- Set last time. - playerData.Tlso=time - + if rho=3 then + + self:_LSOcall(playerData, glideslopeError, lineupError) + elseif diffX > 150 then local wire = 0 @@ -972,10 +933,10 @@ function CARRIERTRAINER:_CallTheBall(playerData) self:_SendMessageToPlayer(hint, 8, playerData) -- Add to debrief. - self:_AddToSummary(playerData, "Calling the Ball", hint) + self:_AddToSummary(playerData, "Bolter or wave off", hint) -- Next step: debrief. - playerData.step = 99 + playerData.step=999 end end @@ -1028,6 +989,105 @@ function CARRIERTRAINER:_Trapped(playerData, pos) end end +--- Entering the Groove. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @param #number glideslopeError Error in degrees. +-- @param #number lineupError Error in degrees. +function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) + + local text="" + + -- Player group. + local player=playerData.unit:GetGroup() + + -- Glideslope high/low calls. + if glideslopeError>1 then + text="You're too high! Throttles back!" + CARRIERTRAINER.LSOcall.HIGHL:ToGroup(player) + elseif glideslopeError>0.5 then + text="You're slightly high. Decrease power." + CARRIERTRAINER.LSOcall.HIGHS:ToGroup(player) + elseif glideslopeError<1.0 then + text="Power! You're way too low." + CARRIERTRAINER.LSOcall.POWERL:ToGroup(player) + elseif glideslopeError<0.5 then + text="You're slightly low. Increase power." + CARRIERTRAINER.LSOcall.POWERS:ToGroup(player) + else + text="Good altitude." + end + + -- Lineup left/right calls. + if lineupError<3 then + text=text.."Come left!" + CARRIERTRAINER.LSOcall.COMELEFTL:ToGroup(player) + elseif lineupError<1 then + text=text.."Come left." + CARRIERTRAINER.LSOcall.COMELEFTS:ToGroup(player) + elseif lineupError>3 then + text=text.."Right for lineup!" + CARRIERTRAINER.LSOcall.RIGHTFORLINEUPL:ToGroup(player) + elseif lineupError>1 then + text=text.."Right for lineup." + CARRIERTRAINER.LSOcall.RIGHTFORLINEUPS:ToGroup(player) + else + text=text.."Good lineup." + end + + -- LSO Message to player. + self:_SendMessageToPlayer(text, 8, playerData, true) + + -- Set last time. + playerData.Tlso=timer.getTime() + +end + +--- Get line up of player wrt to carrier runway. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. +-- @return #number range from Carrier tail to player aircraft in meters. +function CARRIERTRAINER:_Lineup(playerData) + + -- Runway is at an angle of -10 degrees wrt to carrier X direction. + -- TODO: make this carrier dependent + local rwyangle=-10 + local deckheight=22 + local tailpos=-100 + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local diffX, diffZ, rho, phi = self:_GetDistances(playerData.unit) + + -- Position at the end of the deck. From there we calculate the angle. + -- TODO: Check exact number and make carrier dependent. + local b={} + b.x=tailpos + b.z=0 + + -- Position of the aircraft wrt carrier coordinates. + local a={} + a.x=diffX + a.z=diffZ + + --a.x=-200 + --a.y= 0 + --a.z=17.632698070846 --(100)*math.tan(math.rad(10)) + --a.z=20 + --print(a.z) + + -- Vector from plane to ref point on boad. + local c={} + c.x=b.x-a.x + c.y=0 + c.z=b.z-a.z + + -- Current line up and error wrt to final heading of the runway. + local lineup=math.atan2(c.z, c.x) + + return math.deg(lineup), UTILS.VecNorm(c) +end + --------- -- Bla functions @@ -1109,7 +1169,7 @@ function CARRIERTRAINER:_CheckPlayerStatus() -- 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 text=string.format("Welcome back, %s! TCN 74X, ICLS 1, BRC 354 (MAG HDG).\n", playerData.callsign) local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) text=text..string.format("Fly heading %d for %.1f NM to begin your approach.", heading, distance) @@ -1135,13 +1195,13 @@ function CARRIERTRAINER:_CheckPlayerStatus() self:_CheckForLongDownwind(playerData) end self:_Ninety(playerData) - elseif playerData.step == 7 then + elseif playerData.step==7 then self:_Wake(playerData) - elseif playerData.step == 8 then + elseif playerData.step==8 then self:_Groove(playerData) - elseif playerData.step == 9 then - self:_CallTheBall(playerData) - elseif playerData.step == 99 then + elseif playerData.step>=90 and playerData.step<=99 then + self:_CallTheBall(playerData) + elseif playerData.step==999 then self:_Debrief(playerData) end @@ -1315,7 +1375,7 @@ function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData) --TODO: set score and grade. -- Next step debrief. - playerData.step=99 + playerData.step=999 end From c7fdc77ec8a44552a6d7d93f88d64dfb0970cc1e Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 1 Nov 2018 00:09:58 +0100 Subject: [PATCH 007/147] CT v0.1.4 Added aircraft check. Fixed problems with new player. --- .../Moose/Functional/CarrierTrainer.lua | 88 ++++++++++++------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 5053691c5..8480f5948 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -201,7 +201,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.3" +CARRIERTRAINER.version="0.1.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -333,7 +333,7 @@ function CARRIERTRAINER:onafterStatus(From, Event, To) self:_CheckPlayerStatus() -- Call status again in one second. - self:__Status(-1) + self:__Status(-0.5) end --- On after Stop event. Unhandle events and stop status updates. @@ -374,20 +374,29 @@ function CARRIERTRAINER:OnEventBirth(EventData) self:T(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) + + local rightaircraft=false + local aircraft=_unit:GetTypeName() + for _,actype in pairs(CARRIERTRAINER.AircraftType) do + if actype==aircraft then + rightaircraft=true + end + end + if rightaircraft==false then + self:E(string.format("Player aircraft %s not supported of CARRIERTRAINTER.", aircraft)) + return + end + -- Add Menu commands. - self:_AddF10Commands(_unitName) + self:_AddF10Commands(_unitName) -- Init player. - if self.players[_playername]==nil then - self.players[_playername]=self:_InitNewPlayer(_unitName) - else - self:_InitNewRound(self.players[_playername]) - end - + self.players[_playername]=self:_InitNewPlayer(_unitName) + -- Test - CARRIERTRAINER.LSOcall.HIGHL:ToGroup(_group) - CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(_group, 10) - MESSAGE:New(CARRIERTRAINER.LSOcall.HIGHT, 5):ToAllIf(self.Debug) + --CARRIERTRAINER.LSOcall.HIGHL:ToGroup(_group) + --CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(_group, 10) + --MESSAGE:New(CARRIERTRAINER.LSOcall.HIGHT, 5):ToAllIf(self.Debug) end end @@ -440,20 +449,21 @@ 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) + -- Player unit, client and callsign. + playerData.unit = UNIT:FindByName(unitname) + playerData.client = CLIENT:FindByName(unitname, nil, true) playerData.callsign = playerData.unit:GetCallsign() - playerData.totalscore = 0 + -- Total score of player. + playerData.totalscore = playerData.totalscore or 0 -- Number of passes done by player. - playerData.passes=0 + playerData.passes=playerData.passes or 0 - playerData.results={} + playerData.results=playerData.results or {} -- Set difficulty level. - playerData.difficulty=CARRIERTRAINER.Difficulty.NORMAL + playerData.difficulty=playerData.difficulty or CARRIERTRAINER.Difficulty.NORMAL -- Player is in the big zone around the carrier. playerData.inbigzone=playerData.unit:IsInZone(self.giantZone) @@ -1003,38 +1013,49 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) -- Glideslope high/low calls. if glideslopeError>1 then - text="You're too high! Throttles back!" + text="You're too high! Throttles back!" CARRIERTRAINER.LSOcall.HIGHL:ToGroup(player) elseif glideslopeError>0.5 then text="You're slightly high. Decrease power." CARRIERTRAINER.LSOcall.HIGHS:ToGroup(player) - elseif glideslopeError<1.0 then + elseif glideslopeError<-1.0 then text="Power! You're way too low." CARRIERTRAINER.LSOcall.POWERL:ToGroup(player) - elseif glideslopeError<0.5 then + elseif glideslopeError<-0.5 then text="You're slightly low. Increase power." CARRIERTRAINER.LSOcall.POWERS:ToGroup(player) else text="Good altitude." end + + text=text..string.format(" Glideslope Error = %.2f %%", glideslopeError) + text=text.."\n" + + local delay=0 + if math.abs(glideslopeError)>0.5 then + --text=text.."\n" + delay=1.5 + end -- Lineup left/right calls. - if lineupError<3 then + if lineupError<-3 then text=text.."Come left!" - CARRIERTRAINER.LSOcall.COMELEFTL:ToGroup(player) - elseif lineupError<1 then + CARRIERTRAINER.LSOcall.COMELEFTL:ToGroup(player, delay) + elseif lineupError<-1 then text=text.."Come left." - CARRIERTRAINER.LSOcall.COMELEFTS:ToGroup(player) + CARRIERTRAINER.LSOcall.COMELEFTS:ToGroup(player, delay) elseif lineupError>3 then text=text.."Right for lineup!" - CARRIERTRAINER.LSOcall.RIGHTFORLINEUPL:ToGroup(player) + CARRIERTRAINER.LSOcall.RIGHTFORLINEUPL:ToGroup(player, delay) elseif lineupError>1 then text=text.."Right for lineup." - CARRIERTRAINER.LSOcall.RIGHTFORLINEUPS:ToGroup(player) + CARRIERTRAINER.LSOcall.RIGHTFORLINEUPS:ToGroup(player, delay) else text=text.."Good lineup." end + text=text..string.format(" Lineup Error = %.1f %%", lineupError) + -- LSO Message to player. self:_SendMessageToPlayer(text, 8, playerData, true) @@ -1178,7 +1199,9 @@ function CARRIERTRAINER:_CheckPlayerStatus() end if playerData.step==0 and unit:InAir() then - self:_NewRound(playerData) + self:_NewRound(playerData) + -- Jump to Groove for testing. + --playerData.step=8 elseif playerData.step == 1 then self:_Start(playerData) elseif playerData.step == 2 then @@ -1512,8 +1535,8 @@ function CARRIERTRAINER:_InitStennis() -- In the groove self.Groove.name="Groove" self.Groove.Xmin=-4000 - self.Groove.Xmax=100 - self.Groove.Zmin=-2000 + self.Groove.Xmax= 100 + self.Groove.Zmin=-1000 self.Groove.Zmax=nil self.Groove.LimitXmin=nil self.Groove.LimitXmax=nil @@ -1729,8 +1752,9 @@ end -- @param #boolean clear If true, clear screen from previous messages. function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData, clear) if playerData.client then - MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToClient(playerData.client) + --MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToClient(playerData.client) end + MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToAll() end --- Display final score. From 8c320f729ab5d2de497e440997b41d394f12cdc6 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 1 Nov 2018 16:20:49 +0100 Subject: [PATCH 008/147] CT v0.1.4w --- .../Moose/Functional/CarrierTrainer.lua | 240 ++++++++++++------ 1 file changed, 163 insertions(+), 77 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 8480f5948..04675e1d7 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -160,6 +160,13 @@ CARRIERTRAINER.Difficulty={ HARD="TOPGUN Graduate", } +--- Groove data. +-- @type CARRIERTRAINER.GrooveData +-- @field #number AoA Angle of Attack. +-- @field #number Alt Altitude in meters. +-- @field #number GSE Glide slope error in degrees. +-- @field #number LUE Lineup error in degrees. + --- Player data table holding all important parameters for each player. -- @type CARRIERTRAINER.PlayerData -- @field #number id Player ID. @@ -177,6 +184,7 @@ CARRIERTRAINER.Difficulty={ -- @field #boolean waveoff If true, player was waved off. -- @field #boolean calledball If true, player called the ball. -- @field #number Tlso Last time the LSO gave an advice. +-- @field #CARRIERTRAINER.GrooveData Groove data table with elemets of type @{#CARRIERTRAINER.GrooveData}. --- Checkpoint parameters triggering the next step in the pattern. -- @type CARRIERTRAINER.Checkpoint @@ -201,7 +209,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.4" +CARRIERTRAINER.version="0.1.4w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -503,7 +511,7 @@ function CARRIERTRAINER:_NewRound(playerData) if playerData.unit:IsInZone(self.registerZone) then local text="Cleared for approach." - self:_SendMessageToPlayer(text, 10,playerData) + self:_SendMessageToPlayer(text, 10, playerData) self:_InitNewRound(playerData) @@ -512,14 +520,21 @@ function CARRIERTRAINER:_NewRound(playerData) end end ---- Start landing pattern, when player enters the start zone. +--- Start pattern when player enters the start zone. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. function CARRIERTRAINER:_Start(playerData) + -- Check if player is in start zone and about to enter the pattern. 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) + -- Inform player. + local hint = string.format("Entering the pattern.") + if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then + hint=hint.."Aim for 800 feet and 350 kts in the break entry." + end + + -- Send message. self:_SendMessageToPlayer(hint, 8, playerData) -- Next step: upwind. @@ -534,17 +549,18 @@ end 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) + local X, Z = self:_GetDistances(playerData.unit) -- Abort condition check. - if self:_CheckAbort(diffX,diffZ, self.Upwind) then - self:_AbortPattern(playerData, diffX, diffZ, self.Upwind) + if self:_CheckAbort(X, Z, self.Upwind) then + self:_AbortPattern(playerData, X, Z, self.Upwind) return end -- Check if we are in front of the boat (diffX > 0). - if self:_CheckLimits(diffX, diffZ, self.Upwind) then + if self:_CheckLimits(X, Z, self.Upwind) then + -- Get altitiude. local altitude=playerData.unit:GetAltitude() -- Get altitude. @@ -569,22 +585,22 @@ end 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) + local X, Z = self:_GetDistances(playerData.unit) -- Early or late break. local breakpoint = self.BreakEarly - if part == "late" then + if part=="late" then breakpoint = self.BreakLate end -- Check abort conditions. - if self:_CheckAbort(diffX, diffZ, breakpoint) then - self:_AbortPattern(playerData, diffX, diffZ, breakpoint) + if self:_CheckAbort(X, Z, breakpoint) then + self:_AbortPattern(playerData, X, Z, breakpoint) return end -- Check limits. - if self:_CheckLimits(diffX, diffZ, breakpoint) then + if self:_CheckLimits(X, Z, breakpoint) then -- Get current altitude. local altitude=playerData.unit:GetAltitude() @@ -596,14 +612,14 @@ function CARRIERTRAINER:_Break(playerData, part) self:_SendMessageToPlayer(hint, 10, playerData) -- Debrief - if part =="late" then + if part=="late" then self:_AddToSummary(playerData, "Late Break", hint) else - self:_AddToSummary(playerData, "Early Entry", hint) + self:_AddToSummary(playerData, "Early Break", hint) end -- Nest step: late break or abeam. - if (part == "early") then + if part=="early" then playerData.step = 4 else playerData.step = 5 @@ -617,7 +633,7 @@ end 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 X, Z = self:_GetDistances(playerData.unit) -- Get relative heading. local relhead=self:_GetRelativeHeading(playerData.unit) @@ -625,21 +641,21 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) -- One NM from carrier is way too far. local limit = -UTILS.NMToMeters(1) - local text=string.format("Long groove check: diffX=%d, relhead=%.1f", diffX, relhead) + local text=string.format("Long groove check: X=%d, relhead=%.1f", X, relhead) self:T(text) --MESSAGE:New(text, 1):ToAllIf(self.Debug) -- Check we are not too far out w.r.t back of the boat. - if diffX 0) then - if self:_CheckAbort(diffX, diffZ, self.Ninety) then - self:_AbortPattern(playerData, diffX, diffZ, self.Ninety) + --if(Z < -3700 or X < -3700 or X > 0) then + if self:_CheckAbort(X, Z, self.Ninety) then + self:_AbortPattern(playerData, X, Z, self.Ninety) return end @@ -726,6 +741,7 @@ function CARRIERTRAINER:_Ninety(playerData) -- At the 90, i.e. 90 degrees between player heading and BRC of carrier. if relheading<=90 then + -- Get altitude and aoa. local alt=playerData.unit:GetAltitude() local aoa=playerData.unit:GetAoA() @@ -745,12 +761,12 @@ function CARRIERTRAINER:_Ninety(playerData) self:_AddToSummary(playerData, "At the 90", hintFull) -- Long downwind not an issue any more - playerData.longDownwindDone = true + playerData.longDownwindDone=true -- Next step: wake. playerData.step = 7 - elseif relheading>90 and self:_CheckLimits(diffX, diffZ, self.Wake) then + elseif relheading>90 and self:_CheckLimits(X, Z, self.Wake) then -- Message to player. self:_SendMessageToPlayer("You are already at the wake and have not passed the 90! Turn faster next time!", 10, playerData) end @@ -762,16 +778,16 @@ end 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) + local X, Z = self:_GetDistances(playerData.unit) -- Check abort conditions. - if self:_CheckAbort(diffX, diffZ, self.Wake) then - self:_AbortPattern(playerData, diffX, diffZ, self.Wake) + if self:_CheckAbort(X, Z, self.Wake) then + self:_AbortPattern(playerData, X, Z, self.Wake) return end -- Right behind the wake of the carrier dZ>0. - if self:_CheckLimits(diffX, diffZ, self.Wake) then + if self:_CheckLimits(X, Z, self.Wake) then local alt=playerData.unit:GetAltitude() local aoa=playerData.unit:GetAoA() @@ -802,11 +818,11 @@ end function CARRIERTRAINER:_Groove(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) + local X, Z, rho, phi = 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) + if self:_CheckAbort(X, Z, self.Groove) then + self:_AbortPattern(playerData, X, Z, self.Groove) return end @@ -839,6 +855,14 @@ function CARRIERTRAINER:_Groove(playerData) -- Add to debrief. self:_AddToSummary(playerData, "Calling the ball", hintFull) + local groovedata={} --#CARRIERTRAINER.GrooveData + groovedata.Alt=alt + groovedata.AoA=aoa + groovedata.GSE=self:_Glideslope(playerData)-3.5 + groovedata.LUE=self:_Lineup(playerData)-10 + groovedata.Step=playerData.step + table.insert(playerData.Groove, groovedata) + -- Next step. playerData.step = 90 end @@ -849,10 +873,10 @@ 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) +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) + local X, Z, rho, phi = self:_GetDistances(playerData.unit) -- Player altitude local alt=playerData.unit:GetAltitude() @@ -861,8 +885,8 @@ function CARRIERTRAINER:_CallTheBall(playerData) local player=playerData.unit:GetGroup() -- Check abort conditions. - if self:_CheckAbort(diffX, diffZ, self.Trap) then - self:_AbortPattern(playerData, diffX, diffZ, self.Trap) + if self:_CheckAbort(X, Z, self.Trap) then + self:_AbortPattern(playerData, X, Z, self.Trap) return end @@ -872,20 +896,28 @@ function CARRIERTRAINER:_CallTheBall(playerData) local deckheight=22 local tailpos=-100 + -- Lineup with runway centerline. local lineup=self:_Lineup(playerData) local lineupError=lineup-rwyangle - -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. - local h=playerData.unit:GetAltitude()-deckheight - local x=math.abs(diffX-tailpos) - local glideslope=math.atan(h/x) - local glideslopeError=math.deg(glideslope)-3.5 + -- Glide slope. + local glideslope=self:_Glideslope(playerData) + local glideslopeError=glideslope-3.5 --TODO: maybe 3.0? -- Ranges in the groove. - local RRB=UTILS.NMToMeters(0.500) - local RIM=UTILS.NMToMeters(0.375) --0.75/2 - local RIC=UTILS.NMToMeters(0.100) - local RAR=UTILS.NMToMeters(0.050) + local RRB=UTILS.NMToMeters(0.500) -- Roger Ball! call. + local RIM=UTILS.NMToMeters(0.375) -- In the Middle 0.75/2. + local RIC=UTILS.NMToMeters(0.100) -- In Close. + local RAR=UTILS.NMToMeters(0.050) -- At the Ramp. + + -- Data + local groovedata={} --#CARRIERTRAINER.GrooveData + groovedata.Alt=alt + groovedata.AoA=playerData.unit:GetAoA() + groovedata.GSE=glideslopeError + groovedata.LUE=lineupError + groovedata.Step=playerData.step + table.insert(playerData.Groove, groovedata) if rho<=RRB and playerData.step==90 then @@ -926,7 +958,7 @@ function CARRIERTRAINER:_CallTheBall(playerData) self:_LSOcall(playerData, glideslopeError, lineupError) - elseif diffX > 150 then + elseif X > 150 then local wire = 0 local hint = "" @@ -950,6 +982,29 @@ function CARRIERTRAINER:_CallTheBall(playerData) end end +--- Get name of the current pattern step. +-- @param #CARRIERTRAINER self +-- @param #number step Step +-- @return #string Name of the step +function CARRIERTRAINER:_GS(step) + local gp + if step==9 then + gp="X" + elseif step==90 then + gp="IM" + elseif step==91 then + gp="IC" + elseif step==92 then + gp="AR" + elseif step==93 then + + elseif step==94 then + elseif step==95 then + elseif step==96 then + elseif step==97 then + end +end + --- Trapped? -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. @@ -957,7 +1012,7 @@ end 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) + local X, Z, rho, phi = self:_GetDistances(pos) -- Get velocities. local playerVelocity = playerData.unit:GetVelocityKMH() @@ -970,13 +1025,13 @@ function CARRIERTRAINER:_Trapped(playerData, pos) local score = -10 -- Which wire - if(diffX < -14) then + if X < -14 then wire = 1 score = -15 - elseif(diffX < -3) then + elseif X < -3 then wire = 2 score = 10 - elseif (diffX < 10) then + elseif X < 10 then wire = 3 score = 20 else @@ -987,7 +1042,7 @@ function CARRIERTRAINER:_Trapped(playerData, pos) 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) + local text2=string.format("Distance %.1f meters resulted in a %d-wire estimate.", X, wire) MESSAGE:New(text,30):ToAllIf(self.Debug) env.info(text2) @@ -1009,7 +1064,7 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) local text="" -- Player group. - local player=playerData.unit:GetGroup() + local player=playerData.unit:GetGroup() -- Glideslope high/low calls. if glideslopeError>1 then @@ -1054,6 +1109,23 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) text=text.."Good lineup." end + -- Get AoA. + local aoa=playerData.unit:GetAoA() + + if aoa>=9.3 then + text="Your're slow!" + elseif aoa>=8.8 and aoa<9.3 then + text="Your're slightly slow." + elseif aoa>=7.4 and aoa<8.8 then + text="You're on speed." + elseif aoa>=6.9 and aoa<7.4 then + text="You're slightly fast." + elseif aoa>=0 and aoa<6.9 then + text="You're fast!" + else + text="Unknown AoA state." + end + text=text..string.format(" Lineup Error = %.1f %%", lineupError) -- LSO Message to player. @@ -1064,11 +1136,32 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) end +--- Get glide slope of aircraft. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @return #number Glide slope angle in degrees measured from the +function CARRIERTRAINER:_Glideslope(playerData) + + -- Carrier parameters. + local deckheight=22 + local tailpos=-100 + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi = self:_GetDistances(playerData.unit) + + -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. + local h=playerData.unit:GetAltitude()-deckheight + local x=math.abs(X-tailpos) + local glideslope=math.atan(h/x) + + return math.deg(glideslope) +end + --- Get line up of player wrt to carrier runway. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. -- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. --- @return #number range from Carrier tail to player aircraft in meters. +-- @return #number Distance from carrier tail to player aircraft in meters. function CARRIERTRAINER:_Lineup(playerData) -- Runway is at an angle of -10 degrees wrt to carrier X direction. @@ -1078,18 +1171,14 @@ function CARRIERTRAINER:_Lineup(playerData) local tailpos=-100 -- 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 X, Z, rho, phi = self:_GetDistances(playerData.unit) -- Position at the end of the deck. From there we calculate the angle. -- TODO: Check exact number and make carrier dependent. - local b={} - b.x=tailpos - b.z=0 + local b={x=tailpos, z=0} -- Position of the aircraft wrt carrier coordinates. - local a={} - a.x=diffX - a.z=diffZ + local a={x=X, z=Z} --a.x=-200 --a.y= 0 @@ -1098,10 +1187,7 @@ function CARRIERTRAINER:_Lineup(playerData) --print(a.z) -- Vector from plane to ref point on boad. - local c={} - c.x=b.x-a.x - c.y=0 - c.z=b.z-a.z + local c={x=b.x-a.x, y=0, z=b.z-a.z} -- Current line up and error wrt to final heading of the runway. local lineup=math.atan2(c.z, c.x) @@ -1732,7 +1818,7 @@ function CARRIERTRAINER:_AoACheck(playerData, checkpoint, aoa) hint = "You're slightly fast." else --On speed score = 0 - hint = "You're on speed!" + hint = "You're on speed." end hint=hint..string.format(" AoA %.1f = %d %% deviation from %.1f target AoA.", aoa, _error, checkpoint.AoA) @@ -1754,7 +1840,7 @@ function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData, clea if playerData.client then --MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToClient(playerData.client) end - MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToAll() + MESSAGE:New(string.format("%s, %s, %s", self.alias, playerData.callsign, message), duration, nil, clear):ToAll() end --- Display final score. From 3153d196a20d39250d6f04a2ad34609c3712f08d Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 2 Nov 2018 00:29:50 +0100 Subject: [PATCH 009/147] CT v0.1.5 --- .../Moose/Functional/CarrierTrainer.lua | 177 +++++++++--------- 1 file changed, 91 insertions(+), 86 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 04675e1d7..64ee90363 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -182,7 +182,6 @@ CARRIERTRAINER.Difficulty={ -- @field #boolean landed If true, player landed or attempted to land. -- @field #boolean boltered If true, player boltered. -- @field #boolean waveoff If true, player was waved off. --- @field #boolean calledball If true, player called the ball. -- @field #number Tlso Last time the LSO gave an advice. -- @field #CARRIERTRAINER.GrooveData Groove data table with elemets of type @{#CARRIERTRAINER.GrooveData}. @@ -209,7 +208,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.4w" +CARRIERTRAINER.version="0.1.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -327,7 +326,7 @@ function CARRIERTRAINER:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Land) -- Init status check - self:__Status(5) + self:__Status(1) end --- On after Status event. Checks player status. @@ -354,6 +353,81 @@ function CARRIERTRAINER:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Land) 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 74X, ICLS 1, BRC 354 (MAG HDG).\n", playerData.callsign) + local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) + local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) + text=text..string.format("Fly heading %d for %.1f NM to begin your approach.", heading, distance) + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + + if playerData.step==0 and unit:InAir() then + self:_NewRound(playerData) + -- Jump to Groove for testing. + --playerData.step=8 + 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 playerData.longDownwindDone==false 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>=90 and playerData.step<=99 then + self:_CallTheBall(playerData) + elseif playerData.step==999 then + self:_Debrief(playerData) + end + + else + playerData.inbigzone=false + end + + else + -- Unit not alive. + --playerDatas[i] = nil + end + end + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- EVENT functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -495,7 +569,6 @@ function CARRIERTRAINER:_InitNewRound(playerData) playerData.boltered=false playerData.landed=false playerData.waveoff=false - playerData.calledball=false playerData.Tlso=timer.getTime() return playerData end @@ -837,7 +910,6 @@ function CARRIERTRAINER:_Groove(playerData) local aoa = playerData.unit:GetAoA() self:_SendMessageToPlayer("Call the ball.", 8, playerData) - playerData.calledball=true CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(playerData.unit:GetGroup()) -- Grade altitude. @@ -861,6 +933,7 @@ function CARRIERTRAINER:_Groove(playerData) groovedata.GSE=self:_Glideslope(playerData)-3.5 groovedata.LUE=self:_Lineup(playerData)-10 groovedata.Step=playerData.step + playerData.Groove={} table.insert(playerData.Groove, groovedata) -- Next step. @@ -931,6 +1004,7 @@ function CARRIERTRAINER:_CallTheBall(playerData) --TODO: grade for IM self:_SendMessageToPlayer("IM", 8, playerData) + env.info(string.format("FF IM=%d", rho)) playerData.step=92 @@ -938,13 +1012,15 @@ function CARRIERTRAINER:_CallTheBall(playerData) --TODO: grade for IC, call wave off? self:_SendMessageToPlayer("IC", 8, playerData) - + env.info(string.format("FF IC=%d", rho)) + playerData.step=93 elseif rho<=RAR and playerData.step==93 then --TODO: grade for AR self:_SendMessageToPlayer("AR", 8, playerData) + env.info(string.format("FF AR=%d", rho)) playerData.step=94 end @@ -1109,24 +1185,26 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) text=text.."Good lineup." end + text=text..string.format(" Lineup Error = %.1f %%\n", lineupError) + -- Get AoA. local aoa=playerData.unit:GetAoA() if aoa>=9.3 then - text="Your're slow!" + text=text.."Your're slow!" elseif aoa>=8.8 and aoa<9.3 then - text="Your're slightly slow." + text=text.."Your're slightly slow." elseif aoa>=7.4 and aoa<8.8 then - text="You're on speed." + text=text.."You're on speed." elseif aoa>=6.9 and aoa<7.4 then - text="You're slightly fast." + text=text.."You're slightly fast." elseif aoa>=0 and aoa<6.9 then - text="You're fast!" + text=text.."You're fast!" else - text="Unknown AoA state." + text=text.."Unknown AoA state." end - text=text..string.format(" Lineup Error = %.1f %%", lineupError) + -- LSO Message to player. self:_SendMessageToPlayer(text, 8, playerData, true) @@ -1252,80 +1330,7 @@ function CARRIERTRAINER:_GetRelativeHeading(unit) 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 74X, ICLS 1, BRC 354 (MAG HDG).\n", playerData.callsign) - local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) - local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) - text=text..string.format("Fly heading %d for %.1f NM to begin your approach.", heading, distance) - MESSAGE:New(text, 5):ToClient(playerData.client) - - end - - if playerData.step==0 and unit:InAir() then - self:_NewRound(playerData) - -- Jump to Groove for testing. - --playerData.step=8 - 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 playerData.longDownwindDone==false 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>=90 and playerData.step<=99 then - self:_CallTheBall(playerData) - elseif playerData.step==999 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 From 27bf6069d7f488a632ff065ac61779bace68c099 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 2 Nov 2018 16:07:40 +0100 Subject: [PATCH 010/147] CT v0.1.5w --- .../Moose/Functional/CarrierTrainer.lua | 146 +++++++++++++----- 1 file changed, 110 insertions(+), 36 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 64ee90363..f0518f16f 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -160,8 +160,25 @@ CARRIERTRAINER.Difficulty={ HARD="TOPGUN Graduate", } + +--- Groove data. +-- @type CARRIERTRAINER.GroovePos +-- @field #string X At the start. +-- @field #string RB Roger ball. +-- @field #string IM In the middle. +-- @field #string IC In close. +-- @field #string AR At the ramp. +CARRIERTRAINER.GroovePos={ + X="X", + RB="RB", + IM="IM", + IC="IC", + AR="AR", +} + --- Groove data. -- @type CARRIERTRAINER.GrooveData +-- @field #number Step Current step. -- @field #number AoA Angle of Attack. -- @field #number Alt Altitude in meters. -- @field #number GSE Glide slope error in degrees. @@ -183,7 +200,7 @@ CARRIERTRAINER.Difficulty={ -- @field #boolean boltered If true, player boltered. -- @field #boolean waveoff If true, player was waved off. -- @field #number Tlso Last time the LSO gave an advice. --- @field #CARRIERTRAINER.GrooveData Groove data table with elemets of type @{#CARRIERTRAINER.GrooveData}. +-- @field #CARRIERTRAINER.GroovePos Groove data table with elemets of type @{#CARRIERTRAINER.GrooveData}. --- Checkpoint parameters triggering the next step in the pattern. -- @type CARRIERTRAINER.Checkpoint @@ -208,12 +225,20 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.5" +CARRIERTRAINER.version="0.1.5w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Fix radio menu. +-- TODO: Optimized debrief. +-- TODO: Add automatic grading. +-- TODO: Get board numbers. +-- TODO: Add user functions. +-- TODO: Generalize parameters for other carriers and aircraft. +-- TODO: CASE III. + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -306,6 +331,13 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Set difficulty level. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data. +-- @param #CARRIERTRAINER.Difficulty difficulty Difficulty level. +function CARRIERTRAINER:SetDifficulty(playerData, difficulty) + playerData.difficulty=difficulty +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM states @@ -691,7 +723,7 @@ function CARRIERTRAINER:_Break(playerData, part) self:_AddToSummary(playerData, "Early Break", hint) end - -- Nest step: late break or abeam. + -- Next step: late break or abeam. if part=="early" then playerData.step = 4 else @@ -719,7 +751,7 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) --MESSAGE:New(text, 1):ToAllIf(self.Debug) -- Check we are not too far out w.r.t back of the boat. - if Xpos.Zmax then abort=true end + ]] + + -- Abort conditions. + local abortXmin=check.Xmin and (check.Xmin<0 and X<=check.Xmin or check.Xmin>=0 and X>=check.Xmin) + local abortXmax=check.Xmax and (check.Xmax<0 and X>=check.Xmax or check.Xmax>=0 and X<=check.Xmax) + local abortZmin=check.Zmin and (check.Zmin<0 and Z<=check.Zmin or check.Zmin>=0 and Z>=check.Zmin) + local abortZmax=check.Zmax and (check.Zmax<0 and Z>=check.Zmax or check.Zmax>=0 and Z<=check.Zmax) + + -- Check if any of the conditions are met. + local abort=abortXmin or abortXmax or abortZmin or abortZmax return abort end @@ -1432,10 +1489,10 @@ end -- @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. +-- @param #CARRIERTRAINER.Checkpoint posData Checkpoint data. function CARRIERTRAINER:_TooFarOutText(X, Z, posData) - local text="You are too far" + local text="You are too far " local xtext=nil if posData.Xmin and XposData.Zmax then - ztext=" starboard (right)" + ztext="starboard (right)" end if xtext and ztext then @@ -1459,7 +1516,7 @@ function CARRIERTRAINER:_TooFarOutText(X, Z, posData) text=text..ztext end - text=text.." of the carrier!" + text=text.." of the carrier." return text end @@ -1469,14 +1526,14 @@ end -- @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. +-- @param #CARRIERTRAINER.Checkpoint posData Checkpoint data. function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData) -- Text where we are wrong. local toofartext=self:_TooFarOutText(X, Z, posData) -- Send message to player. - self:_SendMessageToPlayer(toofartext.." Abort approach!", 15, playerData, true) + self:_SendMessageToPlayer(toofartext.." Depart and reenter!", 15, playerData, true) -- 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)) @@ -1486,6 +1543,9 @@ function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData) -- Add to debrief. self:_AddToSummary(playerData, "Abort", "Approach aborted.") + -- + playerData.waveoff=true + --TODO: set score and grade. -- Next step debrief. @@ -1530,7 +1590,7 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData) --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)) + --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 @@ -1661,14 +1721,16 @@ end -- @return #boolean If true, checkpoint condition for next step was reached. function CARRIERTRAINER:_CheckLimits(X, Z, check) + -- Limits 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)) + -- Proceed to next step if all conditions are fullfilled. local next=nextXmin and nextXmax and nextZmin and nextZmax - + -- Debug info. 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) @@ -1682,6 +1744,26 @@ end -- MISC functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Grade approach. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @return #string LSO grade. +function CARRIERTRAINER:_LSOgrade(playerData) + + if playerData.waveoff then + + elseif playerData.boltered then + + elseif playerData.landed then + + local gdata=playerData.Groove.X --#CARRIERTRAINER.GrooveData + + else + + end + +end + --- Evaluate player's altitude at checkpoint. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. @@ -1779,10 +1861,10 @@ function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance) 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.") + hint = string.format("You're slightly too far from the boat.") else score = 0 - hint = string.format("with perfect distance to the boat.") + hint = string.format("perfect distance to the boat.") end hint=hint..string.format(" Distance %.1f NM = %d%% deviation from %.1f NM optimal distance.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(checkpoint.Distance)) @@ -2007,14 +2089,6 @@ 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. From 087ac992a2920409c7b1a910d7e275facd827a72 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 4 Nov 2018 01:14:47 +0100 Subject: [PATCH 011/147] CT v0.1.6 --- .../Moose/Functional/CarrierTrainer.lua | 469 +++++++++++------- .../Moose/Wrapper/Controllable.lua | 20 +- .../Moose/Wrapper/Positionable.lua | 15 +- 3 files changed, 311 insertions(+), 193 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index f0518f16f..341a24cec 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -40,7 +40,9 @@ -- @field #CARRIERTRAINER.Checkpoint Wake Right behind the carrier. -- @field #CARRIERTRAINER.Checkpoint Groove In the groove checkpoint. -- @field #CARRIERTRAINER.Checkpoint Trap Landing checkpoint. --- @field +-- @field #number rwyangle Angle of the runway wrt to carrier "nose". For the Stennis ~ -10 degrees. +-- @field #number sterndist Distance in meters from carrier coordinate to the end of the deck. +-- @field #number deckheight Height of the deck in meters. -- @extends Core.Fsm#FSM --- Practice Carrier Landings @@ -55,7 +57,7 @@ -- -- @field #CARRIERTRAINER CARRIERTRAINER = { - ClassName = "CARRIERTRAINER", + ClassName = "CARRIERTRAINER", lid = nil, Debug = true, carrier = nil, @@ -76,6 +78,9 @@ CARRIERTRAINER = { Trap = {}, TACAN = nil, ICLS = nil, + rwyangle = -10, + sterndist =-100, + deckheight = 22, } --- Aircraft types. @@ -160,8 +165,7 @@ CARRIERTRAINER.Difficulty={ HARD="TOPGUN Graduate", } - ---- Groove data. +--- Groove position. -- @type CARRIERTRAINER.GroovePos -- @field #string X At the start. -- @field #string RB Roger ball. @@ -197,8 +201,10 @@ CARRIERTRAINER.GroovePos={ -- @field #string difficulty Difficulty level. -- @field #boolean inbigzone If true, player is in the big zone. -- @field #boolean landed If true, player landed or attempted to land. +-- @field #boolean bolter If true, LSO told player to bolter. -- @field #boolean boltered If true, player boltered. --- @field #boolean waveoff If true, player was waved off. +-- @field #boolean waveoff If true, player was waved off during final approach. +-- @field #boolean patternwo If true, playe was waved of during the pattern. -- @field #number Tlso Last time the LSO gave an advice. -- @field #CARRIERTRAINER.GroovePos Groove data table with elemets of type @{#CARRIERTRAINER.GrooveData}. @@ -225,19 +231,23 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.5w" +CARRIERTRAINER.version="0.1.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Fix radio menu. +-- TODO: Add scoring to radio menu. -- TODO: Optimized debrief. -- TODO: Add automatic grading. -- TODO: Get board numbers. +-- TODO: Get fuel state in pounds. -- TODO: Add user functions. --- TODO: Generalize parameters for other carriers and aircraft. +-- TODO: Generalize parameters for other carriers. +-- TODO: Generalize parameters for other aircraft. +-- TODO: CASE II. -- TODO: CASE III. +-- DONE: Fix radio menu. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -331,13 +341,6 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Set difficulty level. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. --- @param #CARRIERTRAINER.Difficulty difficulty Difficulty level. -function CARRIERTRAINER:SetDifficulty(playerData, difficulty) - playerData.difficulty=difficulty -end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM states @@ -371,8 +374,8 @@ function CARRIERTRAINER:onafterStatus(From, Event, To) -- Check player status. self:_CheckPlayerStatus() - -- Call status again in one second. - self:__Status(-0.5) + -- Call status again in 0.25 seconds. + self:__Status(-0.25) end --- On after Stop event. Unhandle events and stop status updates. @@ -403,7 +406,6 @@ function CARRIERTRAINER:_CheckPlayerStatus() --self:_SendMessageToPlayer("current step "..self:_StepName(playerData.step),1,playerData) --self:_DetailedPlayerStatus(playerData) - --self:_DetailedPlayerStatus(playerData) if unit:IsInZone(self.giantZone) then -- Check if player was previously not inside the zone. @@ -412,15 +414,15 @@ function CARRIERTRAINER:_CheckPlayerStatus() local text=string.format("Welcome back, %s! TCN 74X, ICLS 1, BRC 354 (MAG HDG).\n", playerData.callsign) local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) - text=text..string.format("Fly heading %d for %.1f NM to begin your approach.", heading, distance) + text=text..string.format("Fly heading %d for %.1f NM and turn to BRC.", heading, distance) MESSAGE:New(text, 5):ToClient(playerData.client) end - + if playerData.step==0 and unit:InAir() then self:_NewRound(playerData) -- Jump to Groove for testing. - --playerData.step=8 + playerData.step=8 elseif playerData.step == 1 then self:_Start(playerData) elseif playerData.step == 2 then @@ -453,7 +455,7 @@ function CARRIERTRAINER:_CheckPlayerStatus() else -- Unit not alive. - --playerDatas[i] = nil + self:E(self.lid.."WARNING: Player unit is not alive!") end end end @@ -504,8 +506,8 @@ function CARRIERTRAINER:OnEventBirth(EventData) -- Add Menu commands. self:_AddF10Commands(_unitName) - -- Init player. - self.players[_playername]=self:_InitNewPlayer(_unitName) + -- Init player data. + self.players[_playername]=self:_InitPlayer(_unitName) -- Test --CARRIERTRAINER.LSOcall.HIGHL:ToGroup(_group) @@ -539,9 +541,10 @@ function CARRIERTRAINER:OnEventLand(EventData) self:T(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - -- Check if we caught a wire after one second. - -- TODO: test this! + -- Player data. local playerData=self.players[_playername] --#CARRIERTRAINER.PlayerData + + -- Coordinate at landing event local coord=playerData.unit:GetCoordinate() -- We did land. @@ -555,11 +558,17 @@ function CARRIERTRAINER:OnEventLand(EventData) end end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- CARRIER TRAINING functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Initialize player data. -- @param #CARRIERTRAINER self -- @param #string unitname Name of the player unit. -- @return #CARRIERTRAINER.PlayerData Player data. -function CARRIERTRAINER:_InitNewPlayer(unitname) +function CARRIERTRAINER:_InitPlayer(unitname) local playerData={} --#CARRIERTRAINER.PlayerData @@ -601,14 +610,11 @@ function CARRIERTRAINER:_InitNewRound(playerData) playerData.boltered=false playerData.landed=false playerData.waveoff=false + playerData.patternwo=false playerData.Tlso=timer.getTime() return playerData end -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- CARRIER TRAINING functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - --- Initialize player data. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data. @@ -738,13 +744,13 @@ end function CARRIERTRAINER:_CheckForLongDownwind(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z = self:_GetDistances(playerData.unit) + local X, Z=self:_GetDistances(playerData.unit) -- Get relative heading. local relhead=self:_GetRelativeHeading(playerData.unit) - -- One NM from carrier is way too far. - local limit = -UTILS.NMToMeters(1) + -- One NM from carrier is too far. + local limit=-UTILS.NMToMeters(1) local text=string.format("Long groove check: X=%d, relhead=%.1f", X, relhead) self:T(text) @@ -793,11 +799,6 @@ function CARRIERTRAINER:_Abeam(playerData) -- Check nest step threshold. if self:_CheckLimits(X, Z, self.Abeam) then - - -- Checks: - -- AoA - -- Altitude - -- Distance to carrier. -- Get AoA and altitude. local aoa = playerData.unit:GetAoA() @@ -821,7 +822,7 @@ function CARRIERTRAINER:_Abeam(playerData) -- Add to debrief. self:_AddToSummary(playerData, "Abeam Position", hintFull) - -- Proceed to next step. + -- Next step: ninety. playerData.step = 6 end end @@ -894,6 +895,7 @@ function CARRIERTRAINER:_Wake(playerData) -- Right behind the wake of the carrier dZ>0. if self:_CheckLimits(X, Z, self.Wake) then + -- Get player altitude and AoA. local alt=playerData.unit:GetAltitude() local aoa=playerData.unit:GetAoA() @@ -931,16 +933,16 @@ function CARRIERTRAINER:_Groove(playerData) return end - -- 0 means player is on BRC course but runway heading is -10 degrees. - local heading=self:_GetRelativeHeading(playerData.unit)-10 local calltheball=UTILS.NMToMeters(0.75) if rho<=calltheball then + -- Get player altitude and AoA. local alt = playerData.unit:GetAltitude() local aoa = playerData.unit:GetAoA() + self:_SendMessageToPlayer("Call the ball.", 8, playerData) CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(playerData.unit:GetGroup()) @@ -966,12 +968,14 @@ function CARRIERTRAINER:_Groove(playerData) groovedata.LUE=self:_Lineup(playerData)-10 groovedata.Step=playerData.step + -- Init groove table. playerData.Groove={} - playerData.Groove.X=grovedata - --table.insert(playerData.Groove, groovedata) - -- Next step. - playerData.step = 90 + + playerData.Groove.X=groovedata + + -- Next step: roger ball. + playerData.step=90 end end @@ -997,20 +1001,17 @@ function CARRIERTRAINER:_CallTheBall(playerData) return end - -- Runway is at an angle of -10 degrees wrt to carrier X direction. - -- TODO: make this carrier dependent - local rwyangle=-10 - local deckheight=22 - local tailpos=-100 - -- Lineup with runway centerline. local lineup=self:_Lineup(playerData) - local lineupError=lineup-rwyangle + local lineupError=lineup-self.rwyangle -- Glide slope. local glideslope=self:_Glideslope(playerData) local glideslopeError=glideslope-3.5 --TODO: maybe 3.0? + -- Get AoA. + local AoA=playerData.unit:GetAoA() + -- Ranges in the groove. local RRB=UTILS.NMToMeters(0.500) -- Roger Ball! call. local RIM=UTILS.NMToMeters(0.375) -- In the Middle 0.75/2. @@ -1020,11 +1021,10 @@ function CARRIERTRAINER:_CallTheBall(playerData) -- Data local groovedata={} --#CARRIERTRAINER.GrooveData groovedata.Alt=alt - groovedata.AoA=playerData.unit:GetAoA() + groovedata.AoA=AoA groovedata.GSE=glideslopeError groovedata.LUE=lineupError groovedata.Step=playerData.step - --table.insert(playerData.Groove, groovedata) if rho<=RRB and playerData.step==90 then @@ -1057,10 +1057,25 @@ function CARRIERTRAINER:_CallTheBall(playerData) env.info(string.format("FF IC=%d", rho)) -- Store data. - playerData.Groove.IC=groovedata + playerData.Groove.IC=groovedata + + -- Check if player should wave off. + local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA) + + -- Let's see.. + if waveoff then + + -- Wave off player. + self:_SendMessageToPlayer(CARRIERTRAINER.LSOcall.WAVEOFFT, 10, playerData) + CARRIERTRAINER.LSOcall.WAVEOFF:ToGroup(playerData.unit:GetGroup()) + + -- Next step: debrief. + playerData.step=999 + else + -- Next step: at the ramp. + playerData.step=93 + end - -- Next step: at the ramp. - playerData.step=93 elseif rho<=RAR and playerData.step==93 then @@ -1069,7 +1084,7 @@ function CARRIERTRAINER:_CallTheBall(playerData) env.info(string.format("FF AR=%d", rho)) -- Store data. - playerData.Groove.AR=groovedata + playerData.Groove.AR=groovedata -- Next step: at the ramp. playerData.step=94 @@ -1084,11 +1099,13 @@ function CARRIERTRAINER:_CallTheBall(playerData) self:_LSOcall(playerData, glideslopeError, lineupError) - elseif X > 150 then + elseif X>0 then local wire = 0 local hint = "" local score = 0 + + if playerData.landed then hint = "You boltered." else @@ -1108,6 +1125,31 @@ function CARRIERTRAINER:_CallTheBall(playerData) end end +--- LSO check if player needs to wave off. +-- @param #CARRIERTRAINER self +-- @param #number glideslopeError Glide slope error in degrees. +-- @param #number lineupError Line up error in degrees. +-- @param #number AoA Angle of attack of player aircraft. +-- @return #boolean If true, player should wave off! +function CARRIERTRAINER:_CheckWaveOff(glideslopeError, lineupError, AoA) + + local waveoff=false + + if math.abs(glideslopeError)>3 then + waveoff=true + end + + if math.abs(lineupError)>3 then + waveoff=true + end + + if AoA<6.9 or AoA>9.3 then + waveoff=true + end + + return waveoff +end + --- Get name of the current pattern step. -- @param #CARRIERTRAINER self -- @param #number step Step @@ -1140,10 +1182,6 @@ function CARRIERTRAINER:_Trapped(playerData, pos) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi = self:_GetDistances(pos) - -- Get velocities. - local playerVelocity = playerData.unit:GetVelocityKMH() - local carrierVelocity = self.carrier:GetVelocityKMH() - if playerData.unit:InAir()==false then -- Seems we have successfully landed. @@ -1151,14 +1189,14 @@ function CARRIERTRAINER:_Trapped(playerData, pos) local score = -10 -- Which wire - if X < -14 then - wire = 1 + if X<-14 then + wire = 1 score = -15 - elseif X < -3 then - wire = 2 + elseif X<-3 then + wire = 2 score = 10 - elseif X < 10 then - wire = 3 + elseif X<10 then + wire = 3 score = 20 else wire = 4 @@ -1177,6 +1215,7 @@ function CARRIERTRAINER:_Trapped(playerData, pos) else --Boltered! + playerData.boltered=true end end @@ -1194,22 +1233,22 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) -- Glideslope high/low calls. if glideslopeError>1 then - text="You're too high! Throttles back!" + text="You're high!" CARRIERTRAINER.LSOcall.HIGHL:ToGroup(player) elseif glideslopeError>0.5 then - text="You're slightly high. Decrease power." + text="You're a little high." CARRIERTRAINER.LSOcall.HIGHS:ToGroup(player) elseif glideslopeError<-1.0 then - text="Power! You're way too low." + text="Power!" CARRIERTRAINER.LSOcall.POWERL:ToGroup(player) elseif glideslopeError<-0.5 then - text="You're slightly low. Increase power." + text="You're a little low." CARRIERTRAINER.LSOcall.POWERS:ToGroup(player) else text="Good altitude." end - text=text..string.format(" Glideslope Error = %.2f %%", glideslopeError) + text=text..string.format(" Glideslope Error = %.2f°", glideslopeError) text=text.."\n" local delay=0 @@ -1235,7 +1274,7 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) text=text.."Good lineup." end - text=text..string.format(" Lineup Error = %.1f %%\n", lineupError) + text=text..string.format(" Lineup Error = %.1f°\n", lineupError) -- Get AoA. local aoa=playerData.unit:GetAoA() @@ -1243,23 +1282,24 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) if aoa>=9.3 then text=text.."Your're slow!" elseif aoa>=8.8 and aoa<9.3 then - text=text.."Your're slightly slow." + text=text.."Your're a little slow." elseif aoa>=7.4 and aoa<8.8 then text=text.."You're on speed." elseif aoa>=6.9 and aoa<7.4 then - text=text.."You're slightly fast." + text=text.."You're a little fast." elseif aoa>=0 and aoa<6.9 then text=text.."You're fast!" else text=text.."Unknown AoA state." end + + text=text..string.format(" AoA = %.1f", aoa) -- LSO Message to player. - self:_SendMessageToPlayer(text, 8, playerData, true) + self:_SendMessageToPlayer(text, 5, playerData, false) -- Set last time. - playerData.Tlso=timer.getTime() - + playerData.Tlso=timer.getTime() end --- Get glide slope of aircraft. @@ -1268,16 +1308,12 @@ end -- @return #number Glide slope angle in degrees measured from the function CARRIERTRAINER:_Glideslope(playerData) - -- Carrier parameters. - local deckheight=22 - local tailpos=-100 - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi = self:_GetDistances(playerData.unit) -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. - local h=playerData.unit:GetAltitude()-deckheight - local x=math.abs(X-tailpos) + local h=playerData.unit:GetAltitude()-self.deckheight + local x=math.abs(X-self.sterndist) local glideslope=math.atan(h/x) return math.deg(glideslope) @@ -1290,18 +1326,11 @@ end -- @return #number Distance from carrier tail to player aircraft in meters. function CARRIERTRAINER:_Lineup(playerData) - -- Runway is at an angle of -10 degrees wrt to carrier X direction. - -- TODO: make this carrier dependent - local rwyangle=-10 - local deckheight=22 - local tailpos=-100 - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi = self:_GetDistances(playerData.unit) -- Position at the end of the deck. From there we calculate the angle. - -- TODO: Check exact number and make carrier dependent. - local b={x=tailpos, z=0} + local b={x=self.sterndist, z=0} -- Position of the aircraft wrt carrier coordinates. local a={x=X, z=Z} @@ -1456,11 +1485,10 @@ end -- @param #CARRIERTRAINER self -- @param #number X X distance player to carrier. -- @param #number Z Z distance player to carrier. --- @param #table pos Position data limits. +-- @param #CARRIERTRAINER.Checkpoint pos Position data limits. -- @return #boolean If true, approach should be aborted. -function CARRIERTRAINER:_CheckAbort(X, Z, check) +function CARRIERTRAINER:_CheckAbort(X, Z, pos) - --[[ local abort=false if pos.Xmin and Xpos.Zmax then abort=true end - ]] + --[[ -- Abort conditions. local abortXmin=check.Xmin and (check.Xmin<0 and X<=check.Xmin or check.Xmin>=0 and X>=check.Xmin) local abortXmax=check.Xmax and (check.Xmax<0 and X>=check.Xmax or check.Xmax>=0 and X<=check.Xmax) @@ -1481,7 +1509,8 @@ function CARRIERTRAINER:_CheckAbort(X, Z, check) -- Check if any of the conditions are met. local abort=abortXmin or abortXmax or abortZmin or abortZmax - + ]] + return abort end @@ -1543,8 +1572,8 @@ function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData) -- Add to debrief. self:_AddToSummary(playerData, "Abort", "Approach aborted.") - -- - playerData.waveoff=true + -- Pattern wave off! + playerData.patternwo=true --TODO: set score and grade. @@ -1567,13 +1596,6 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData) local dist=playerData.unit:GetCoordinate():Get2DDistance(self.carrier:GetCoordinate()) local dx,dz,rho,phi=self:_GetDistances(unit) - -- Player and carrier position vector. - local playerPosition = playerData.unit:GetVec3() - local carrierPosition = self.carrier:GetVec3() - - local diffZ = playerPosition.z - carrierPosition.z - local diffX = playerPosition.x - carrierPosition.x - local heading=unit:GetCoordinate():HeadingTo(self.startZone:GetCoordinate()) local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() @@ -1581,16 +1603,13 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData) local relhead=self:_GetRelativeHeading(playerData.unit) - local text=string.format("%s, current AoA=%.1f\n", playerData.callsign, aoa) - text=text..string.format("velo x=%.1f y=%.1f z=%.1f\n", velo.x, velo.y, velo.z) - text=text..string.format("wind x=%.1f y=%.1f z=%.1f\n", wind.x, wind.y, wind.z) - text=text..string.format("pitch=%.1f | roll=%.1f | yaw=%.1f | climb=%.1f\n", pitch, roll, yaw, unit:GetClimbAnge()) + local text=string.format("%s, current step: %.1f\n", playerData.callsign, self:_StepName(playerData.step)) + text=text..string.format("AoA=%.1f | Vx=%.1f Vy=%.1f Vz=%.1f\n", aoa, velo.x, velo.y, velo.z) + text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) + text=text..string.format("pitch=%.1f | roll=%.1f | yaw=%.1f | climb=%.1f\n", pitch, roll, yaw, unit:GetClimbAngle()) text=text..string.format("relheading=%.1f degrees\n", relhead) + text=text..string.format("Distance: X=%d m Z=%d m", dx, dz) text=text..string.format("rho=%.1f m phi=%.1f degrees\n", rho,phi) - --text=text..string.format("current step = %d %s\n", playerData.step, self:_StepName(playerData.step)) - --text=text..string.format("Carrier distance: d=%d m\n", dist) - --text=text..string.format("Carrier distance: x=%d m z=%d m sum=%d (old)\n", diffX, diffZ, math.abs(diffX)+math.abs(diffZ)) - --text=text..string.format("Carrier distance: x=%d m z=%d m sum=%d (new)", dx, dz, math.abs(dz)+math.abs(dx)) MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) end @@ -1599,6 +1618,7 @@ end -- @param #CARRIERTRAINER self function CARRIERTRAINER:_InitStennis() + -- Upwind leg self.Upwind.name="Upwind" self.Upwind.Xmin=-4000 -- TODO Should be withing 4 km behind carrier. Why? @@ -1616,7 +1636,7 @@ function CARRIERTRAINER:_InitStennis() -- Early break self.BreakEarly.name="Early Break" self.BreakEarly.Xmin=-500 - self.BreakEarly.Xmax=nil + self.BreakEarly.Xmax=4000 self.BreakEarly.Zmin=-3700 self.BreakEarly.Zmax=1500 self.BreakEarly.LimitXmin=0 @@ -1630,7 +1650,7 @@ function CARRIERTRAINER:_InitStennis() -- Late break self.BreakLate.name="Late Break" self.BreakLate.Xmin=-500 - self.BreakLate.Xmax=nil + self.BreakLate.Xmax=4000 self.BreakLate.Zmin=-3700 self.BreakLate.Zmax=1500 self.BreakLate.LimitXmin=0 @@ -1864,7 +1884,7 @@ function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance) hint = string.format("You're slightly too far from the boat.") else score = 0 - hint = string.format("perfect distance to the boat.") + hint = string.format("Perfect distance to the boat.") end hint=hint..string.format(" Distance %.1f NM = %d%% deviation from %.1f NM optimal distance.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(checkpoint.Distance)) @@ -2051,34 +2071,33 @@ function CARRIERTRAINER:_AddF10Commands(_unitName) CARRIERTRAINER.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "Carrier Trainer") end + -- Player Data. local playerData=self.players[playername] -- F10/Carrier Trainer/ local _trainPath = missionCommands.addSubMenuForGroup(_gid, self.alias, CARRIERTRAINER.MenuF10[_gid]) + -- F10/Carrier Trainer//Results - --local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Results", _trainPath) - -- F10/Carrier Trainer//My Settings - local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _trainPath) + local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Results", _trainPath) + -- F10/Carrier Trainer//My Settings/Difficulty - local _difficulPath = missionCommands.addSubMenuForGroup(_gid, "Difficulty", _settingsPath) - -- F10/Carrier Trainer//Carrier Info - local _infoPath = missionCommands.addSubMenuForGroup(_gid, "Carrier Info", _trainPath) + local _difficulPath = missionCommands.addSubMenuForGroup(_gid, "Difficulty", _trainPath) - -- F10/Carrier Trainer//Stats/ - --missionCommands.addCommandForGroup(_gid, "All Results", _statsPath, self._DisplayStrafePitResults, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "My Results", _statsPath, self._DisplayBombingResults, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "Reset All Results", _statsPath, self._ResetRangeStats, self, _unitName) - -- F10/Carrier Trainer//My Settings/ - --missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) - -- F10/Carrier Trainer//My Settings/Difficulty - missionCommands.addCommandForGroup(_gid, "Flight Student", _difficulPath, self.SetDifficulty, self, playerData, CARRIERTRAINER.Difficulty.EASY) - missionCommands.addCommandForGroup(_gid, "Naval Aviator", _difficulPath, self.SetDifficulty, self, playerData, CARRIERTRAINER.Difficulty.NORMAL) - missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _difficulPath, self.SetDifficulty, self, playerData, CARRIERTRAINER.Difficulty.HARD) - -- F10/Carrier Trainer//Carrier Info/ - missionCommands.addCommandForGroup(_gid, "Carrier Info", _infoPath, self._DisplayCarrierInfo, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayCarrierWeather, self, _unitName) + -- F10/Carrier Trainer//Results/ + -- TODO: Add result functions. + --missionCommands.addCommandForGroup(_gid, "All Results", _statsPath, self._DisplayStrafePitResults, self, _unitName) + --missionCommands.addCommandForGroup(_gid, "My Results", _statsPath, self._DisplayBombingResults, self, _unitName) + --missionCommands.addCommandForGroup(_gid, "(Clear ALL Results)", _statsPath, self._ResetRangeStats, self, _unitName) + + -- F10/Carrier Trainer//Difficulty + missionCommands.addCommandForGroup(_gid, "Flight Student", _difficulPath, self._SetDifficulty, self, playername, CARRIERTRAINER.Difficulty.EASY) + missionCommands.addCommandForGroup(_gid, "Naval Aviator", _difficulPath, self._SetDifficulty, self, playername, CARRIERTRAINER.Difficulty.NORMAL) + missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _difficulPath, self._SetDifficulty, self, playername, CARRIERTRAINER.Difficulty.HARD) + + -- F10/Carrier Trainer// + missionCommands.addCommandForGroup(_gid, "Carrier Info", _trainPath, self._DisplayCarrierInfo, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Weather Report", _trainPath, self._DisplayCarrierWeather, self, _unitName) + --TODO: Flare carrier. end else self:T(self.lid.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) @@ -2089,11 +2108,87 @@ function CARRIERTRAINER:_AddF10Commands(_unitName) end + +--- Display top 10 player scores. +-- @param #CARRIERTRAINER self +-- @param #string _unitName Name fo the player unit. +function CARRIERTRAINER:_DisplayScoreBoard(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + + -- Results table. + local _playerResults={} + + -- Message text. + local _message = string.format("Greenie Board:\n") + + -- Loop over player results. + for _playerName,_results in pairs(self.strafePlayerResults) do + + -- Get the best result of the player. + local _best=nil + for _,_result in pairs(_results) do + if _best==nil or _result.hits > _best.hits then + _best = _result + end + end + + -- Add best result to table. + if _best ~= nil then + local text=string.format("%s: Hits %i - %s - %s", _playerName, _best.hits, _best.zone.name, _best.text) + table.insert(_playerResults,{msg = text, hits = _best.hits}) + end + + end + + --Sort list! + local _sort = function( a,b ) return a.hits > b.hits end + table.sort(_playerResults,_sort) + + -- Add top 10 results. + for _i = 1, math.min(#_playerResults, self.ndisplayresult) do + _message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg) + end + + -- In case there are no scores yet. + if #_playerResults<1 then + _message = _message.."No player scored yet." + end + + -- Send message. + self:_DisplayMessageToGroup(_unit, _message, nil, true) + end +end + + +--- Set difficulty level. +-- @param #CARRIERTRAINER self +-- @param #string playernaame Player name. +-- @param #CARRIERTRAINER.Difficulty difficulty Difficulty level. +function CARRIERTRAINER:_SetDifficulty(playername, difficulty) + self:E({difficulty=difficulty, playername=playername}) + + local playerData=self.players[playername] --CARRIERTRAINER.PlayerData + + if playerData then + playerData.difficulty=difficulty + local text=string.format("Your difficulty level is now: %s.", difficulty) + self:_SendMessageToPlayer(text, 5, playerData) + else + self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + end +end + --- Report information about carrier. -- @param #CARRIERTRAINER self -- @param #string _unitname Name of the player unit. function CARRIERTRAINER:_DisplayCarrierInfo(_unitname) - self:F(_unitname) + self:E(_unitname) -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) @@ -2101,35 +2196,43 @@ function CARRIERTRAINER:_DisplayCarrierInfo(_unitname) -- Check if we have a player. if unit and playername then - -- Message text. - local text=string.format("%s info:\n", self.alias) - - -- Current coordinates. - local coord=self.carrier:GetCoordinate() - + -- Player data. local playerData=self.players[playername] --#CARRIERTRAINER.PlayerData - local carrierheading=self.carrier:GetHeading() - local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocity()) - - text=text..string.format("BRC %d\n", carrierheading) - text=text..string.format("Speed %d kts\n", carrierspeed) + if playerData then - - local tacan="unknown" - local icls="unknown" - if self.TACAN~=nil then - tacan=tostring(self.TACAN) - end - if self.ICLS~=nil then - icls=tostring(self.ICLS) - end - - text=text..string.format("TACAN Channel %s", tacan) - text=text..string.format("ICLS Channel %s", icls) - - self:_SendMessageToPlayer(text, 20, playerData) + -- Message text. + local text=string.format("%s info:\n", self.alias) + -- Current coordinates. + local coord=self.carrier:GetCoordinate() + + -- Carrier speed and heading. + local carrierheading=self.carrier:GetHeading() + local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocityMPS()) + + -- Tacan/ICLS. + local tacan="unknown" + local icls="unknown" + if self.TACAN~=nil then + tacan=tostring(self.TACAN) + end + if self.ICLS~=nil then + icls=tostring(self.ICLS) + end + + -- Message text + text=text..string.format("BRC %d°\n", carrierheading) + text=text..string.format("Speed %d kts\n", carrierspeed) + text=text..string.format("TACAN Channel %s\n", tacan) + text=text..string.format("ICLS Channel %s", icls) + + -- Send message. + self:_SendMessageToPlayer(text, 20, playerData) + + else + self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + end end end @@ -2139,10 +2242,11 @@ end -- @param #CARRIERTRAINER self -- @param #string _unitname Name of the player unit. function CARRIERTRAINER:_DisplayCarrierWeather(_unitname) - self:F(_unitname) + self:E(_unitname) -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) + self:E({playername=playername}) -- Check if we have a player. if unit and playername then @@ -2153,11 +2257,10 @@ function CARRIERTRAINER:_DisplayCarrierWeather(_unitname) -- Current coordinates. local coord=self.carrier:GetCoordinate() - -- Get atmospheric data at range location. - local position=self.location --Core.Point#COORDINATE - local T=position:GetTemperature() - local P=position:GetPressure() - local Wd,Ws=position:GetWind() + -- Get atmospheric data at carrier location. + local T=coord:GetTemperature() + local P=coord:GetPressure() + local Wd,Ws=coord:GetWind() -- Get Beaufort wind scale. local Bn,Bd=UTILS.BeaufortScale(Ws) @@ -2177,24 +2280,22 @@ function CARRIERTRAINER:_DisplayCarrierWeather(_unitname) tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) tP=string.format("%.2f inHg", P*hPa2inHg) end - - - -- Message text. - text=text..string.format("Weather Report at %s:\n", self.rangename) + + -- Report text. + text=text..string.format("Weather Report at Carrier %s:\n", self.alias) text=text..string.format("--------------------------------------------------\n") text=text..string.format("Temperature %s\n", tT) text=text..string.format("Wind from %s at %s (%s)\n", WD, tW, Bd) text=text..string.format("QFE %.1f hPa = %s", P, tP) - - - -- Send message to player group. - --self:_DisplayMessageToGroup(unit, text, nil, true) - self:_SendMessageToPlayer(text, 30, self.players[playername]) - + -- Debug output. self:T2(self.lid..text) + + -- Send message to player group. + self:_SendMessageToPlayer(text, 30, self.players[playername]) + else - self:T(self.lid..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) + self:E(self.lid..string.format("ERROR! Could not find player unit in CarrierWeather! Unit name = %s", _unitname)) end end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index d57f7fac4..d349aa88d 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -342,16 +342,26 @@ function CONTROLLABLE:PushTask( DCSTask, WaitTime ) local DCSControllable = self:GetDCSObject() if DCSControllable then - local Controller = self:_GetController() + + local DCSControllableName = self:GetName() -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. -- Therefore we schedule the functions to set the mission and options for the Controllable. - -- Controller:pushTask( DCSTask ) + -- Controller:pushTask( DCSTask ) + + local function PushTask( Controller, DCSTask ) + if self and self:IsAlive() then + local Controller = self:_GetController() + Controller:pushTask( DCSTask ) + else + BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) + end + end - if WaitTime then - self.TaskScheduler:Schedule( Controller, Controller.pushTask, { DCSTask }, WaitTime ) + if not WaitTime or WaitTime == 0 then + PushTask( self, DCSTask ) else - Controller:pushTask( DCSTask ) + self.TaskScheduler:Schedule( self, PushTask, { DCSTask }, WaitTime ) end return self diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index fef546f38..137d4ab4c 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -706,8 +706,8 @@ end --- Returns the unit's climb or descent angle. -- @param Wrapper.Positionable#POSITIONABLE self --- @return #number Climb or descent angle in degrees. -function POSITIONABLE:GetClimbAnge() +-- @return #number Climb or descent angle in degrees. Or 0 if velocity vector norm is zero (or nil). Or nil, if the position of the POSITIONABLE returns nil. +function POSITIONABLE:GetClimbAngle() -- Get position of the unit. local unitpos = self:GetPosition() @@ -719,10 +719,17 @@ function POSITIONABLE:GetClimbAnge() if unitvel and UTILS.VecNorm(unitvel)~=0 then - return math.asin(unitvel.y/UTILS.VecNorm(unitvel)) - + -- Calculate climb angle. + local angle=math.asin(unitvel.y/UTILS.VecNorm(unitvel)) + + -- Return angle in degrees. + return math.deg(angle) + else + return 0 end end + + return nil end --- Returns the pitch angle of a unit. From 1febe765bf10792ddbeca0e2850355771d75b740 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 5 Nov 2018 00:35:48 +0100 Subject: [PATCH 012/147] CT v0.1.7 --- Moose Development/Moose/Core/Point.lua | 4 +- Moose Development/Moose/Core/SpawnStatic.lua | 43 +++ .../Moose/Functional/CarrierTrainer.lua | 300 +++++++++++------- Moose Development/Moose/Functional/Range.lua | 25 +- 4 files changed, 245 insertions(+), 127 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 862dc3839..25efa066e 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -460,12 +460,12 @@ do -- COORDINATE --- Add a Distance in meters from the COORDINATE orthonormal plane, with the given angle, and calculate the new COORDINATE. -- @param #COORDINATE self -- @param DCS#Distance Distance The Distance to be added in meters. - -- @param DCS#Angle Angle The Angle in degrees. + -- @param DCS#Angle Angle The Angle in degrees. Defaults to 0 if not specified (nil). -- @return #COORDINATE The new calculated COORDINATE. function COORDINATE:Translate( Distance, Angle ) local SX = self.x local SY = self.z - local Radians = Angle / 180 * math.pi + local Radians = (Angle or 0) / 180 * math.pi local TX = Distance * math.cos( Radians ) + SX local TY = Distance * math.sin( Radians ) + SY diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index 0081195a5..e5a7b4e59 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -195,6 +195,49 @@ function SPAWNSTATIC:SpawnFromPointVec2( PointVec2, Heading, NewName ) --R2.1 end +--- Creates a new @{Static} from a COORDINATE. +-- @param #SPAWNSTATIC self +-- @param Core.Point#COORDINATE Coordinate The 3D coordinate where to spawn the static. +-- @param #number Heading (Optional) Heading The heading of the static, which is a number in degrees from 0 to 360. Default is 0 degrees. +-- @param #string NewName (Optional) The name of the new static. +-- @return #SPAWNSTATIC +function SPAWNSTATIC:SpawnFromCoordinate(Coordinate, Heading, NewName) --R2.4 + self:F( { PointVec2, Heading, NewName } ) + + local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix ) + + if StaticTemplate then + + Heading=Heading or 0 + + local StaticUnitTemplate = StaticTemplate.units[1] + + StaticUnitTemplate.x = Coordinate.x + StaticUnitTemplate.y = Coordinate.z + StaticUnitTemplate.alt = Coordinate.y + + StaticTemplate.route = nil + StaticTemplate.groupId = nil + + StaticTemplate.name = NewName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex ) + StaticUnitTemplate.name = StaticTemplate.name + StaticUnitTemplate.heading = ( Heading / 180 ) * math.pi + + _DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID) + + self:F({StaticTemplate = StaticTemplate}) + + local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] ) + + self.SpawnIndex = self.SpawnIndex + 1 + + return _DATABASE:FindStatic(Static:getName()) + end + + return nil +end + + --- Respawns the original @{Static}. -- @param #SPAWNSTATIC self -- @return #SPAWNSTATIC diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 341a24cec..cffeb5a38 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -14,7 +14,7 @@ -- -- === -- --- ### Authors: **Bankler** (original idea and script), **funkyfranky** (MOOSE class implementation and enhancements) +-- ### Authors: **funkyfranky** (MOOSE class implementation and enhancements), **Bankler** (original idea and script) -- -- @module Functional.CarrierTrainer -- @image MOOSE.JPG @@ -151,7 +151,7 @@ CARRIERTRAINER.LSOcall={ BOLTER=USERSOUND:New("LSO - Bolter.ogg"), BOLTERT="Bolter, Bolter!", LONGGROOVE=USERSOUND:New("LSO - Long in Groove.ogg"), - LONGGROOVET="You're lon in the groove. Depart and re-enter.", + LONGGROOVET="You're long in the groove. Depart and re-enter.", } --- Difficulty level. @@ -160,24 +160,28 @@ CARRIERTRAINER.LSOcall={ -- @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", + EASY="Flight Student", NORMAL="Naval Aviator", HARD="TOPGUN Graduate", } --- Groove position. -- @type CARRIERTRAINER.GroovePos --- @field #string X At the start. +-- @field #string X0 Entering the groove. +-- @field #string XX At the start, i.e. 3/4 from the run down. -- @field #string RB Roger ball. -- @field #string IM In the middle. -- @field #string IC In close. -- @field #string AR At the ramp. +-- @field #string IW In the wires. CARRIERTRAINER.GroovePos={ - X="X", + X0="X0", + XX="X", RB="RB", IM="IM", IC="IC", AR="AR", + IW="IW", } --- Groove data. @@ -187,18 +191,18 @@ CARRIERTRAINER.GroovePos={ -- @field #number Alt Altitude in meters. -- @field #number GSE Glide slope error in degrees. -- @field #number LUE Lineup error in degrees. +-- @field #number Roll Roll angle. ---- Player data table holding all important parameters for each player. +--- Player data table holding all important parameters of each player. -- @type CARRIERTRAINER.PlayerData --- @field #number id Player ID. --- @field Wrapper.Unit#UNIT unit Aircraft unit of the player. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. -- @field #string callsign Callsign of player. +-- @field #string difficulty Difficulty level. -- @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 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 bolter If true, LSO told player to bolter. @@ -206,7 +210,7 @@ CARRIERTRAINER.GroovePos={ -- @field #boolean waveoff If true, player was waved off during final approach. -- @field #boolean patternwo If true, playe was waved of during the pattern. -- @field #number Tlso Last time the LSO gave an advice. --- @field #CARRIERTRAINER.GroovePos Groove data table with elemets of type @{#CARRIERTRAINER.GrooveData}. +-- @field #CARRIERTRAINER.GroovePos groove Data table at each position in the groove. Elemets are of type @{#CARRIERTRAINER.GrooveData}. --- Checkpoint parameters triggering the next step in the pattern. -- @type CARRIERTRAINER.Checkpoint @@ -231,7 +235,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.6" +CARRIERTRAINER.version="0.1.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -257,7 +261,7 @@ CARRIERTRAINER.version="0.1.6" -- @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 +-- @return #CARRIERTRAINER self or nil if carrier unit does not exist. function CARRIERTRAINER:New(carriername, alias) -- Inherit everthing from FSM class. @@ -267,13 +271,15 @@ function CARRIERTRAINER:New(carriername, alias) self.carrier=UNIT:FindByName(carriername) if self.carrier then + -- Carrier zones. 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 + -- Carrier unit does not exist error. 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) + self:E(text) return nil end @@ -411,46 +417,66 @@ function CARRIERTRAINER:_CheckPlayerStatus() -- Check if player was previously not inside the zone. if playerData.inbigzone==false then + -- Welcome player once he enters the carrier zone. local text=string.format("Welcome back, %s! TCN 74X, ICLS 1, BRC 354 (MAG HDG).\n", playerData.callsign) + + -- Heading and distance to register for approach. local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) + + -- Send message. text=text..string.format("Fly heading %d for %.1f NM and turn to BRC.", heading, distance) MESSAGE:New(text, 5):ToClient(playerData.client) end - + if playerData.step==0 and unit:InAir() then + -- New approach. self:_NewRound(playerData) - -- Jump to Groove for testing. - playerData.step=8 + + -- Jump to Groove for testing. + if self.groovedebug then + playerData.step=8 + self.groovedebug=false + end elseif playerData.step == 1 then + -- Entering the pattern. self:_Start(playerData) elseif playerData.step == 2 then + -- Upwind leg. self:_Upwind(playerData) elseif playerData.step == 3 then + -- Early break. self:_Break(playerData, "early") elseif playerData.step == 4 then + -- Late break. self:_Break(playerData, "late") elseif playerData.step == 5 then + -- Abeam position. self:_Abeam(playerData) elseif playerData.step == 6 then -- Check long down wind leg. - if playerData.longDownwindDone==false then - self:_CheckForLongDownwind(playerData) - end + self:_CheckForLongDownwind(playerData) + -- At the ninety. self:_Ninety(playerData) elseif playerData.step==7 then + -- In the wake. self:_Wake(playerData) elseif playerData.step==8 then + -- Entering the groove. self:_Groove(playerData) elseif playerData.step>=90 and playerData.step<=99 then - self:_CallTheBall(playerData) + -- In the groove. + self:_CallTheBall(playerData) elseif playerData.step==999 then - self:_Debrief(playerData) + -- Debriefing. + SCHEDULER:New(nil, self._Debrief, {self,playerData}, 10) + --SCHEDULER:New(self:_Debrief(playerData) + playerData.step=-1 end else - playerData.inbigzone=false + playerData.inbigzone=false end else @@ -508,12 +534,10 @@ function CARRIERTRAINER:OnEventBirth(EventData) -- Init player data. self.players[_playername]=self:_InitPlayer(_unitName) - - -- Test - --CARRIERTRAINER.LSOcall.HIGHL:ToGroup(_group) - --CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(_group, 10) - --MESSAGE:New(CARRIERTRAINER.LSOcall.HIGHT, 5):ToAllIf(self.Debug) - + + -- Start in the groove for debugging. + self.groovedebug=false + end end @@ -547,19 +571,27 @@ function CARRIERTRAINER:OnEventLand(EventData) -- Coordinate at landing event local coord=playerData.unit:GetCoordinate() + -- Debug mark of player landing coord. + local lp=coord:MarkToAll("Landing coord.") + coord:SmokeGreen() + + -- Debug marks of wires. + local w1=self.carrier:GetCoordinate():Translate(-104, 0):MarkToAll("Wire 1") + local w2=self.carrier:GetCoordinate():Translate( -92, 0):MarkToAll("Wire 2") + local w3=self.carrier:GetCoordinate():Translate( -80, 0):MarkToAll("Wire 3") + local w4=self.carrier:GetCoordinate():Translate( -68, 0):MarkToAll("Wire 4") + -- We did land. playerData.landed=true --TODO: maybe check that we actually landed on the right carrier. - -- Call trapped function in 5 seconds to make sure we did not bolter. - SCHEDULER:New(nil, self._Trapped,{self, playerData, coord}, 5) + -- Call trapped function in 3 seconds to make sure we did not bolter. + SCHEDULER:New(nil, self._Trapped,{self, playerData, coord}, 3) end end - - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- CARRIER TRAINING functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -570,6 +602,7 @@ end -- @return #CARRIERTRAINER.PlayerData Player data. function CARRIERTRAINER:_InitPlayer(unitname) + -- Player data. local playerData={} --#CARRIERTRAINER.PlayerData -- Player unit, client and callsign. @@ -602,15 +635,16 @@ end -- @param #CARRIERTRAINER.PlayerData playerData Player data. -- @return #CARRIERTRAINER.PlayerData Initialized player data. function CARRIERTRAINER:_InitNewRound(playerData) + self:I(self.lid..string.format("New round for player %s.", playerData.callsign)) playerData.step=0 playerData.score=100 - playerData.grade={} + playerData.groove={} playerData.debrief={} - playerData.longDownwindDone=false + playerData.patternwo=false + playerData.waveoff=false + playerData.bolter=false playerData.boltered=false playerData.landed=false - playerData.waveoff=false - playerData.patternwo=false playerData.Tlso=timer.getTime() return playerData end @@ -642,7 +676,7 @@ function CARRIERTRAINER:_Start(playerData) -- Inform player. local hint = string.format("Entering the pattern.") if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then - hint=hint.."Aim for 800 feet and 350 kts in the break entry." + hint=hint.."Aim for 800 feet and 350 kts at the break entry." end -- Send message. @@ -750,7 +784,7 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) local relhead=self:_GetRelativeHeading(playerData.unit) -- One NM from carrier is too far. - local limit=-UTILS.NMToMeters(1) + local limit=-UTILS.NMToMeters(1.5) local text=string.format("Long groove check: X=%d, relhead=%.1f", X, relhead) self:T(text) @@ -772,9 +806,6 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) playerData.score=playerData.score-40 local grade="LIG PATTERN WAVE OFF - CUT 1 PT" - - -- Long downwind done! - playerData.longDownwindDone = true -- Next step: Debriefing. playerData.step=999 @@ -835,7 +866,7 @@ function CARRIERTRAINER:_Ninety(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z = self:_GetDistances(playerData.unit) - --if(Z < -3700 or X < -3700 or X > 0) then + -- Check abort conditions. if self:_CheckAbort(X, Z, self.Ninety) then self:_AbortPattern(playerData, X, Z, self.Ninety) return @@ -866,9 +897,6 @@ function CARRIERTRAINER:_Ninety(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 @@ -932,20 +960,22 @@ function CARRIERTRAINER:_Groove(playerData) self:_AbortPattern(playerData, X, Z, self.Groove) return end + + -- Call the ball distance. + local calltheball=UTILS.NMToMeters(0.75)+math.abs(self.sterndist) + + local relhead=self:_GetRelativeHeading(playerData.unit)+self.rwyangle + local lineup=self:_Lineup(playerData)-self.rwyangle + local roll=playerData.unit:GetRoll() + env.info(string.format("FF relhead=%d lineup=%d roll=%d", relhead, lineup, roll)) - local calltheball=UTILS.NMToMeters(0.75) - - if rho<=calltheball then + if math.abs(lineup)<5 and math.abs(relhead)<10 then -- Get player altitude and AoA. local alt = playerData.unit:GetAltitude() local aoa = playerData.unit:GetAoA() - - self:_SendMessageToPlayer("Call the ball.", 8, playerData) - CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(playerData.unit:GetGroup()) - -- Grade altitude. local hintAlt=self:_AltitudeCheck(playerData, self.Groove, alt) @@ -959,22 +989,20 @@ function CARRIERTRAINER:_Groove(playerData) self:_SendMessageToPlayer(hintFull, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, "Calling the ball", hintFull) + self:_AddToSummary(playerData, "Enter Groove", hintFull) + -- Gather pilot data. local groovedata={} --#CARRIERTRAINER.GrooveData groovedata.Alt=alt groovedata.AoA=aoa groovedata.GSE=self:_Glideslope(playerData)-3.5 - groovedata.LUE=self:_Lineup(playerData)-10 + groovedata.LUE=self:_Lineup(playerData)-self.rwyangle groovedata.Step=playerData.step - -- Init groove table. - playerData.Groove={} + -- Groove + playerData.groove.X0=groovedata - - playerData.Groove.X=groovedata - - -- Next step: roger ball. + -- Next step: X call the ball. playerData.step=90 end @@ -1013,51 +1041,63 @@ function CARRIERTRAINER:_CallTheBall(playerData) local AoA=playerData.unit:GetAoA() -- Ranges in the groove. - local RRB=UTILS.NMToMeters(0.500) -- Roger Ball! call. - local RIM=UTILS.NMToMeters(0.375) -- In the Middle 0.75/2. - local RIC=UTILS.NMToMeters(0.100) -- In Close. - local RAR=UTILS.NMToMeters(0.050) -- At the Ramp. + local RXX=UTILS.NMToMeters(0.750)+math.abs(self.sterndist) -- Start of groove. 0.75 = 1389 m + local RRB=UTILS.NMToMeters(0.500)+math.abs(self.sterndist) -- Roger Ball! call. 0.5 = 926 m + local RIM=UTILS.NMToMeters(0.375)+math.abs(self.sterndist) -- In the Middle 0.75/2. 0.375 = 695 m + local RIC=UTILS.NMToMeters(0.100)+math.abs(self.sterndist) -- In Close. 0.1 = 185 m + local RAR=UTILS.NMToMeters(0.000)+math.abs(self.sterndist) -- At the Ramp. -- Data local groovedata={} --#CARRIERTRAINER.GrooveData + groovedata.Step=playerData.step groovedata.Alt=alt groovedata.AoA=AoA groovedata.GSE=glideslopeError groovedata.LUE=lineupError - groovedata.Step=playerData.step - if rho<=RRB and playerData.step==90 then + if rho<=RXX and playerData.step==90 then + + -- LSO "Call the ball" call. + self:_SendMessageToPlayer("Call the ball.", 8, playerData) + CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(playerData.unit:GetGroup()) + playerData.Tlso=timer.getTime() + + -- Next step: roger ball. + playerData.step=91 + + elseif rho<=RRB and playerData.step==91 then - -- Roger ball! + -- Pilot: "Roger ball" call. self:_SendMessageToPlayer(CARRIERTRAINER.LSOcall.ROGERBALLT, 8, playerData) CARRIERTRAINER.LSOcall.ROGERBALL:ToGroup(player) + playerData.Tlso=timer.getTime()+1 -- Store data. - playerData.Groove.RB=groovedata + playerData.groove.RB=groovedata -- Next step: in the middle. - playerData.step=91 + playerData.step=92 - elseif rho<=RIM and playerData.step==91 then + elseif rho<=RIM and playerData.step==92 then --TODO: grade for IM self:_SendMessageToPlayer("IM", 8, playerData) env.info(string.format("FF IM=%d", rho)) -- Store data. - playerData.Groove.IM=groovedata + playerData.groove.IM=groovedata -- Next step: in close. - playerData.step=92 + playerData.step=93 - elseif rho<=RIC and playerData.step==92 then + elseif rho<=RIC and playerData.step==93 then --TODO: grade for IC, call wave off? self:_SendMessageToPlayer("IC", 8, playerData) env.info(string.format("FF IC=%d", rho)) -- Store data. - playerData.Groove.IC=groovedata + playerData.groove.IC=groovedata -- Check if player should wave off. local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA) @@ -1068,26 +1108,28 @@ function CARRIERTRAINER:_CallTheBall(playerData) -- Wave off player. self:_SendMessageToPlayer(CARRIERTRAINER.LSOcall.WAVEOFFT, 10, playerData) CARRIERTRAINER.LSOcall.WAVEOFF:ToGroup(playerData.unit:GetGroup()) + playerData.Tlso=timer.getTime() -- Next step: debrief. playerData.step=999 + + return else -- Next step: at the ramp. - playerData.step=93 + playerData.step=94 end - - elseif rho<=RAR and playerData.step==93 then + elseif rho<=RAR and playerData.step==94 then --TODO: grade for AR self:_SendMessageToPlayer("AR", 8, playerData) env.info(string.format("FF AR=%d", rho)) -- Store data. - playerData.Groove.AR=groovedata + playerData.groove.AR=groovedata -- Next step: at the ramp. - playerData.step=94 + playerData.step=95 end -- Time since last LSO call. @@ -1095,31 +1137,35 @@ function CARRIERTRAINER:_CallTheBall(playerData) local deltaT=time-playerData.Tlso -- Check if we are beween 3/4 NM and end of ship. - if rho=3 then + if rho>=RAR and rho=3 then + -- LSO call if necessary. self:_LSOcall(playerData, glideslopeError, lineupError) elseif X>0 then - - local wire = 0 - local hint = "" - local score = 0 - - + if playerData.landed then - hint = "You boltered." + + local hint="You boltered." + + -- Send message to player. + self:_SendMessageToPlayer(hint, 8, playerData) + + -- Add to debrief. + self:_AddToSummary(playerData, "Bolter", hint) + else - hint = "You were waved off." - wire = -1 - score = -10 + + local hint="You were waved off." + + -- Send message to player. + self:_SendMessageToPlayer(hint, 8, playerData) + + -- Add to debrief. + self:_AddToSummary(playerData, "Wave Off", hint) + end - - -- Send message to player. - self:_SendMessageToPlayer(hint, 8, playerData) - - -- Add to debrief. - self:_AddToSummary(playerData, "Bolter or wave off", hint) - + -- Next step: debrief. playerData.step=999 end @@ -1135,14 +1181,17 @@ function CARRIERTRAINER:_CheckWaveOff(glideslopeError, lineupError, AoA) local waveoff=false + -- Too high or too low? if math.abs(glideslopeError)>3 then waveoff=true end + -- Too far from centerline? if math.abs(lineupError)>3 then waveoff=true end + -- Too slow or too fast? if AoA<6.9 or AoA>9.3 then waveoff=true end @@ -1188,25 +1237,26 @@ function CARRIERTRAINER:_Trapped(playerData, pos) local wire = 1 local score = -10 - -- Which wire - if X<-14 then - wire = 1 - score = -15 - elseif X<-3 then - wire = 2 - score = 10 - elseif X<10 then - wire = 3 - score = 20 - else - wire = 4 - score = 7 - end + -- Little offset for the exact wire positions. + local wdx=11 + -- Which wire was caught? + if X<-104+wdx then + wire=1 + elseif X<-92+wdx then + wire=2 + elseif X<-80+wdx then + wire=3 + elseif X<68+wdx then + wire=4 + else + wire=0 + 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.", X, wire) + local text2=string.format("Distance X=%.1f meters resulted in a %d-wire estimate.", X, wire) MESSAGE:New(text,30):ToAllIf(self.Debug) env.info(text2) @@ -1217,6 +1267,9 @@ function CARRIERTRAINER:_Trapped(playerData, pos) --Boltered! playerData.boltered=true end + + -- Next step: debriefing. + playerData.step=999 end --- Entering the Groove. @@ -1371,7 +1424,7 @@ function CARRIERTRAINER:_Debrief(playerData) -- Debriefing text. local text=string.format("Debriefing:\n") - text=text..string.format("===========\n\n") + text=text..string.format("===================================================\n") for _,_data in pairs(playerData.debrief) do local step=_data.step local comment=_data.hint @@ -1618,7 +1671,20 @@ end -- @param #CARRIERTRAINER self function CARRIERTRAINER:_InitStennis() + -- Carrier Parameters. + self.rwyangle = -10 + self.sterndist =-150 + self.deckheight = 22 + --[[ + q0=self.carrier:GetCoordinate():SetAltitude(25) + q0:BigSmokeSmall(0.1) + q1=self.carrier:GetCoordinate():Translate(-104,0):SetAltitude(22) --1st wire + q1:BigSmokeSmall(0.1)--:SmokeGreen() + q2=self.carrier:GetCoordinate():Translate(-68,0):SetAltitude(22) --4th wire ==> distance between wires 12 m + q2:BigSmokeSmall(0.1)--:SmokeBlue() + ]] + -- Upwind leg self.Upwind.name="Upwind" self.Upwind.Xmin=-4000 -- TODO Should be withing 4 km behind carrier. Why? @@ -1776,7 +1842,7 @@ function CARRIERTRAINER:_LSOgrade(playerData) elseif playerData.landed then - local gdata=playerData.Groove.X --#CARRIERTRAINER.GrooveData + local gdata=playerData.groove.X --#CARRIERTRAINER.GrooveData else @@ -1947,7 +2013,9 @@ function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData, clea if playerData.client then --MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToClient(playerData.client) end - MESSAGE:New(string.format("%s, %s, %s", self.alias, playerData.callsign, message), duration, nil, clear):ToAll() + local text=string.format("%s, %s, %s", self.alias, playerData.callsign, message) + MESSAGE:New(text, duration, nil, clear):ToAll() + env.info(text) end --- Display final score. diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 1825e262a..394f134f2 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.2" +RANGE.version="1.2.3" --TODO list: --TODO: Add custom weapons, which can be specified by the user. @@ -460,9 +460,10 @@ function RANGE:SetBombtrackThreshold(distance) self.BombtrackThreshold=distance*1000 or 25*1000 end ---- Set range location. If this is not done, one (random) unit position of the range is used to determine the center of the range. +--- Set range location. If this is not done, one (random) unit position of the range is used to determine the location of the range. +-- The range location determines the position at which the weather data is evaluated. -- @param #RANGE self --- @param Core.Point#COORDINATE coordinate Coordinate of the center of the range. +-- @param Core.Point#COORDINATE coordinate Coordinate of the range. function RANGE:SetRangeLocation(coordinate) self.location=coordinate end @@ -471,7 +472,7 @@ end -- If a zone is not explicitly specified, the range zone is determined by its location and radius. -- @param #RANGE self -- @param Core.Zone#ZONE zone MOOSE zone defining the range perimeters. -function RANGE:SetRangeLocation(zone) +function RANGE:SetRangeZone(zone) self.rangezone=zone end @@ -1161,15 +1162,21 @@ function RANGE:OnEventShot(EventData) local _callsign=self:_myname(_unitName) -- Coordinate of impact point. - local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) + local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) + + -- Check if impact happend in range zone. + local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) -- Distance from range. We dont want to smoke targets outside of the range. local impactdist=impactcoord:Get2DDistance(self.location) - --impactcoord:MarkToAll("Bomb impact point") + -- Impact point of bomb. + if self.Debug then + impactcoord:MarkToAll("Bomb impact point") + end -- Smoke impact point of bomb. - if self.PlayerSettings[_playername].smokebombimpact and impactdist Date: Mon, 5 Nov 2018 16:18:15 +0100 Subject: [PATCH 013/147] CTv0.1.7w --- .../Moose/Functional/CarrierTrainer.lua | 136 ++++++++++++++++-- 1 file changed, 124 insertions(+), 12 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index cffeb5a38..2e3bfecb4 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -235,7 +235,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.7" +CARRIERTRAINER.version="0.1.7w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1064,6 +1064,9 @@ function CARRIERTRAINER:_CallTheBall(playerData) -- Next step: roger ball. playerData.step=91 + + -- Store data. + playerData.groove.XX=groovedata elseif rho<=RRB and playerData.step==91 then @@ -1128,7 +1131,7 @@ function CARRIERTRAINER:_CallTheBall(playerData) -- Store data. playerData.groove.AR=groovedata - -- Next step: at the ramp. + -- Next step: in the wires. playerData.step=95 end @@ -1172,6 +1175,11 @@ function CARRIERTRAINER:_CallTheBall(playerData) end --- LSO check if player needs to wave off. +-- Wave off conditions are: +-- +-- * Glide slope error > 3 degrees. +-- * Line up error > 3 degrees. +-- * AoA<6.9 or AoA>9.3. -- @param #CARRIERTRAINER self -- @param #number glideslopeError Glide slope error in degrees. -- @param #number lineupError Line up error in degrees. @@ -1205,21 +1213,22 @@ end -- @return #string Name of the step function CARRIERTRAINER:_GS(step) local gp - if step==9 then - gp="X" - elseif step==90 then - gp="IM" + if step==90 then + gp="X0" -- Entering the groove. elseif step==91 then - gp="IC" + gp="XX" -- Starting the groove. elseif step==92 then - gp="AR" + gp="RB" -- Roger ball call. elseif step==93 then - + gp="IM" -- In the middle. elseif step==94 then + gp="IC" -- In close. elseif step==95 then + gp="AR" -- At the ramp. elseif step==96 then - elseif step==97 then + gp="IW" -- In the wires. end + return gp end --- Trapped? @@ -1424,7 +1433,7 @@ function CARRIERTRAINER:_Debrief(playerData) -- Debriefing text. local text=string.format("Debriefing:\n") - text=text..string.format("===================================================\n") + text=text..string.format("================================\n") for _,_data in pairs(playerData.debrief) do local step=_data.step local comment=_data.hint @@ -1836,13 +1845,28 @@ end -- @return #string LSO grade. function CARRIERTRAINER:_LSOgrade(playerData) + local grade="" + + if playerData.patternwo then + return + end + if playerData.waveoff then elseif playerData.boltered then elseif playerData.landed then - local gdata=playerData.groove.X --#CARRIERTRAINER.GrooveData + --local gdata=playerData.groove.XX --#CARRIERTRAINER.GrooveData + + + grade=grade..playerData.groove.XX + grade=grade..playerData.groove.RB + grade=grade..playerData.groove.IM + grade=grade..playerData.groove.IC + grade=grade..playerData.groove.AR + grade=grade..playerData.groove.IW + else @@ -1850,6 +1874,94 @@ function CARRIERTRAINER:_LSOgrade(playerData) end +--- Grade approach. +-- @param #CARRIERTRAINER self +-- @param #CARRIERTRAINER.GrooveData fdata Flight data in the groove. +-- @return #string LSO grade. +function CARRIERTRAINER:_Flightdata2Text(fdata) + + local function little(text) + return string.format("(%s)",text) + end + local function underline(text) + return string.format("_%s_", text) + end + + if fdata==nil then + return "" + end + + local step=fdata.Step + local AOA=fdata.AoA + local GSE=fdata.GSE + local LUE=fdata.LUE + local ROL=fdata.Roll + + local Y=self:_GS(step) + + local S=nil + if AOA>9.3 then + S=underline("F") + elseif AOA>8.7 then + S="F" + elseif AOA>8.3 then + S=little("F") + elseif AOA<6.7 then + S=underline("SLO") + elseif AOA<7.7 then + S="SLO" + elseif AOA<7.9 then + S=little("SLO") + end + + local A=nil + if GSE>1 then + A=underline("H") + elseif GSE>0.5 then + A=little("H") + elseif GSE>0.25 then + A="H" + elseif GSE<-1 then + A=underline("LO") + elseif GSE<-0.5 then + A=little("LO") + elseif GSE<-0.25 then + A="LO" + end + + local D=nil + if LUE>3 then + D=underline("LUL") + elseif LUE>1 then + D="LUL" + elseif LUE>0.5 then + D=little("LUL") + elseif LUE<-3 then + D=underline("LUR") + elseif LUE<-1 then + D="LUR" + elseif LUE<-0.5 then + D=little("LUL") + end + + local G="" + if S then + G=G..S + end + if A then + G=G..A + end + if D then + G=G..D + end + + if G~="" then + G=G..Y + end + + return G +end + --- Evaluate player's altitude at checkpoint. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. From 9fac65f31f6a6a8380004fdb567accd22be301b4 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 6 Nov 2018 00:04:52 +0100 Subject: [PATCH 014/147] CT v0.1..8 --- .../Moose/Functional/CarrierTrainer.lua | 483 ++++++++---------- 1 file changed, 222 insertions(+), 261 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 2e3bfecb4..a17c30d0a 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -66,16 +66,16 @@ CARRIERTRAINER = { registerZone = nil, startZone = nil, giantZone = nil, - players = {}, - menuadded = {}, - Upwind = {}, - Abeam = {}, - BreakEarly = {}, - BreakLate = {}, - Ninety = {}, - Wake = {}, - Groove = {}, - Trap = {}, + players = {}, + menuadded = {}, + Upwind = {}, + Abeam = {}, + BreakEarly = {}, + BreakLate = {}, + Ninety = {}, + Wake = {}, + Groove = {}, + Trap = {}, TACAN = nil, ICLS = nil, rwyangle = -10, @@ -193,6 +193,12 @@ CARRIERTRAINER.GroovePos={ -- @field #number LUE Lineup error in degrees. -- @field #number Roll Roll angle. +--- LSO grade +-- @type CARRIERDATA.LSOgrade +-- @field #string grade LSO grade string +-- @field #string details Detailed step analysis. +-- @field #number points Points received. + --- Player data table holding all important parameters of each player. -- @type CARRIERTRAINER.PlayerData -- @field Wrapper.Client#CLIENT client Client object of player. @@ -201,8 +207,9 @@ CARRIERTRAINER.GroovePos={ -- @field #string difficulty Difficulty level. -- @field #number score Player score of the current pass. -- @field #number passes Number of passes. +-- @field #boolan attitudemonitor If true, display aircraft attitude and other parameters constantly. -- @field #table debrief Debrief analysis of the current step of this pass. --- @field #table results Results of all passes. +-- @field #table grade LSO grade of passes. -- @field #boolean inbigzone If true, player is in the big zone. -- @field #boolean landed If true, player landed or attempted to land. -- @field #boolean bolter If true, LSO told player to bolter. @@ -235,7 +242,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.7w" +CARRIERTRAINER.version="0.1.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -409,8 +416,10 @@ function CARRIERTRAINER:_CheckPlayerStatus() if unit:IsAlive() then - --self:_SendMessageToPlayer("current step "..self:_StepName(playerData.step),1,playerData) - --self:_DetailedPlayerStatus(playerData) + -- Display aircraft attitude and other parameters as message text. + if playerData.attitudemonitor then + self:_DetailedPlayerStatus(playerData) + end if unit:IsInZone(self.giantZone) then @@ -436,7 +445,7 @@ function CARRIERTRAINER:_CheckPlayerStatus() -- Jump to Groove for testing. if self.groovedebug then - playerData.step=8 + playerData.step=90 self.groovedebug=false end elseif playerData.step == 1 then @@ -462,16 +471,15 @@ function CARRIERTRAINER:_CheckPlayerStatus() elseif playerData.step==7 then -- In the wake. self:_Wake(playerData) - elseif playerData.step==8 then + elseif playerData.step==90 then -- Entering the groove. self:_Groove(playerData) - elseif playerData.step>=90 and playerData.step<=99 then + elseif playerData.step>=91 and playerData.step<=99 then -- In the groove. self:_CallTheBall(playerData) elseif playerData.step==999 then -- Debriefing. SCHEDULER:New(nil, self._Debrief, {self,playerData}, 10) - --SCHEDULER:New(self:_Debrief(playerData) playerData.step=-1 end @@ -605,18 +613,19 @@ function CARRIERTRAINER:_InitPlayer(unitname) -- Player data. local playerData={} --#CARRIERTRAINER.PlayerData - -- Player unit, client and callsign. + -- Player unit, client and callsign. playerData.unit = UNIT:FindByName(unitname) playerData.client = CLIENT:FindByName(unitname, nil, true) playerData.callsign = playerData.unit:GetCallsign() - -- Total score of player. - playerData.totalscore = playerData.totalscore or 0 - -- Number of passes done by player. playerData.passes=playerData.passes or 0 - playerData.results=playerData.results or {} + -- LSO grades. + playerData.grades=playerData.grades or {} + + -- Attitude monitor. + playerData.attitudemonitor=false -- Set difficulty level. playerData.difficulty=playerData.difficulty or CARRIERTRAINER.Difficulty.NORMAL @@ -709,13 +718,13 @@ function CARRIERTRAINER:_Upwind(playerData) local altitude=playerData.unit:GetAltitude() -- Get altitude. - local hint=self:_AltitudeCheck(playerData, self.Upwind, altitude) + local hint, debrief=self:_AltitudeCheck(playerData, self.Upwind, altitude) -- Message to player - self:_SendMessageToPlayer(hint, 8, playerData) + self:_SendMessageToPlayer(hint, 10, playerData) -- Debrief. - self:_AddToSummary(playerData, "Entering the Break", hint) + self:_AddToSummary(playerData, "Entering the Break", debrief) -- Next step. playerData.step=3 @@ -751,16 +760,16 @@ function CARRIERTRAINER:_Break(playerData, part) local altitude=playerData.unit:GetAltitude() -- Grade altitude. - local hint=self:_AltitudeCheck(playerData, breakpoint, altitude) + local hint, debrief=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) + self:_AddToSummary(playerData, "Late Break", debrief) else - self:_AddToSummary(playerData, "Early Break", hint) + self:_AddToSummary(playerData, "Early Break", debrief) end -- Next step: late break or abeam. @@ -800,10 +809,7 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) CARRIERTRAINER.LSOcall.LONGGROOVE:ToGroup(playerData.unit:GetGroup()) -- Debrief. - self:_AddToSummary(playerData, "Long in the groove", "bla") - - -- Decrease score. - playerData.score=playerData.score-40 + self:_AddToSummary(playerData, "Downwind", "Long in the groove.") local grade="LIG PATTERN WAVE OFF - CUT 1 PT" @@ -835,26 +841,27 @@ function CARRIERTRAINER:_Abeam(playerData) 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) + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, self.Abeam, alt) + + -- Grade AoA. + local hintAoA, debriefAoA=self:_AoACheck(playerData, self.Abeam, aoa) -- Grade distance to carrier. - local hintDist=self:_DistanceCheck(playerData, self.Abeam, math.abs(Z)) + local hintDist, debriefDist=self:_DistanceCheck(playerData, self.Abeam, math.abs(Z)) -- Compile full hint. - local hintFull=string.format("%s\n%s\n%s", hintAlt, hintAoA, hintDist) + local hint=string.format("%s\n%s\n%s", hintAlt, hintAoA, hintDist) + local debrief=string.format("%s\n%s\n%s", debriefAlt, debriefAoA, debriefDist) -- Send message to playerr. - self:_SendMessageToPlayer(hintFull, 10, playerData) + self:_SendMessageToPlayer(hint, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, "Abeam Position", hintFull) + self:_AddToSummary(playerData, "Abeam Position", debrief) -- Next step: ninety. - playerData.step = 6 + playerData.step=6 end end @@ -883,22 +890,23 @@ function CARRIERTRAINER:_Ninety(playerData) local aoa=playerData.unit:GetAoA() -- Grade altitude. - local hintAlt=self:_AltitudeCheck(playerData, self.Ninety, alt) + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, self.Ninety, alt) -- Grade AoA. - local hintAoA=self:_AoACheck(playerData, self.Ninety, aoa) + local hintAoA, debriefAoA=self:_AoACheck(playerData, self.Ninety, aoa) -- Compile full hint. - local hintFull=string.format("%s\n%s", hintAlt, hintAoA) + local hint=string.format("%s\n%s", hintAlt, hintAoA) + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) -- Message to player. - self:_SendMessageToPlayer(hintFull, 10, playerData) + self:_SendMessageToPlayer(hint, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, "At the 90", hintFull) + self:_AddToSummary(playerData, "At the 90", debrief) -- Next step: wake. - playerData.step = 7 + playerData.step=7 elseif relheading>90 and self:_CheckLimits(X, Z, self.Wake) then -- Message to player. @@ -928,22 +936,23 @@ function CARRIERTRAINER:_Wake(playerData) local aoa=playerData.unit:GetAoA() -- Grade altitude. - local hintAlt=self:_AltitudeCheck(playerData, self.Wake, alt) + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, self.Wake, alt) -- Grade AoA. - local hintAoA=self:_AoACheck(playerData, self.Wake, aoa) + local hintAoA, debriefAoA=self:_AoACheck(playerData, self.Wake, aoa) -- Compile full hint. - local hintFull=string.format("%s\n%s", hintAlt, hintAoA) - + local hint=string.format("%s\n%s", hintAlt, hintAoA) + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) + -- Message to player. - self:_SendMessageToPlayer(hintFull, 10, playerData) + self:_SendMessageToPlayer(hint, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, "At the Wake", hintFull) + self:_AddToSummary(playerData, "At the Wake", debrief) -- Next step: Groove. - playerData.step = 8 + playerData.step=90 end end @@ -960,9 +969,6 @@ function CARRIERTRAINER:_Groove(playerData) self:_AbortPattern(playerData, X, Z, self.Groove) return end - - -- Call the ball distance. - local calltheball=UTILS.NMToMeters(0.75)+math.abs(self.sterndist) local relhead=self:_GetRelativeHeading(playerData.unit)+self.rwyangle local lineup=self:_Lineup(playerData)-self.rwyangle @@ -977,19 +983,20 @@ function CARRIERTRAINER:_Groove(playerData) local aoa = playerData.unit:GetAoA() -- Grade altitude. - local hintAlt=self:_AltitudeCheck(playerData, self.Groove, alt) + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, self.Groove, alt) -- AoA feed back - local hintAoA=self:_AoACheck(playerData, self.Groove, aoa) + local hintAoA, debriefAoA=self:_AoACheck(playerData, self.Groove, aoa) -- Compile full hint. - local hintFull=string.format("%s\n%s", hintAlt, hintAoA) - + local hint=string.format("%s\n%s", hintAlt, hintAoA) + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) + -- Message to player. - self:_SendMessageToPlayer(hintFull, 10, playerData) + self:_SendMessageToPlayer(hint, 10, playerData) -- Add to debrief. - self:_AddToSummary(playerData, "Enter Groove", hintFull) + self:_AddToSummary(playerData, "Enter Groove", debrief) -- Gather pilot data. local groovedata={} --#CARRIERTRAINER.GrooveData @@ -997,13 +1004,14 @@ function CARRIERTRAINER:_Groove(playerData) groovedata.AoA=aoa groovedata.GSE=self:_Glideslope(playerData)-3.5 groovedata.LUE=self:_Lineup(playerData)-self.rwyangle + groovedata.Roll=roll groovedata.Step=playerData.step -- Groove playerData.groove.X0=groovedata -- Next step: X call the ball. - playerData.step=90 + playerData.step=91 end end @@ -1054,21 +1062,22 @@ function CARRIERTRAINER:_CallTheBall(playerData) groovedata.AoA=AoA groovedata.GSE=glideslopeError groovedata.LUE=lineupError + groovedata.Roll=playerData.unit:GetRoll() - if rho<=RXX and playerData.step==90 then + if rho<=RXX and playerData.step==91 then -- LSO "Call the ball" call. self:_SendMessageToPlayer("Call the ball.", 8, playerData) CARRIERTRAINER.LSOcall.CALLTHEBALL:ToGroup(playerData.unit:GetGroup()) playerData.Tlso=timer.getTime() + + -- Store data. + playerData.groove.XX=groovedata -- Next step: roger ball. - playerData.step=91 - - -- Store data. - playerData.groove.XX=groovedata + playerData.step=92 - elseif rho<=RRB and playerData.step==91 then + elseif rho<=RRB and playerData.step==92 then -- Pilot: "Roger ball" call. self:_SendMessageToPlayer(CARRIERTRAINER.LSOcall.ROGERBALLT, 8, playerData) @@ -1079,9 +1088,9 @@ function CARRIERTRAINER:_CallTheBall(playerData) playerData.groove.RB=groovedata -- Next step: in the middle. - playerData.step=92 + playerData.step=93 - elseif rho<=RIM and playerData.step==92 then + elseif rho<=RIM and playerData.step==93 then --TODO: grade for IM self:_SendMessageToPlayer("IM", 8, playerData) @@ -1091,9 +1100,9 @@ function CARRIERTRAINER:_CallTheBall(playerData) playerData.groove.IM=groovedata -- Next step: in close. - playerData.step=93 + playerData.step=94 - elseif rho<=RIC and playerData.step==93 then + elseif rho<=RIC and playerData.step==94 then --TODO: grade for IC, call wave off? self:_SendMessageToPlayer("IC", 8, playerData) @@ -1119,10 +1128,10 @@ function CARRIERTRAINER:_CallTheBall(playerData) return else -- Next step: at the ramp. - playerData.step=94 + playerData.step=95 end - elseif rho<=RAR and playerData.step==94 then + elseif rho<=RAR and playerData.step==95 then --TODO: grade for AR self:_SendMessageToPlayer("AR", 8, playerData) @@ -1132,7 +1141,7 @@ function CARRIERTRAINER:_CallTheBall(playerData) playerData.groove.AR=groovedata -- Next step: in the wires. - playerData.step=95 + playerData.step=96 end -- Time since last LSO call. @@ -1375,7 +1384,7 @@ function CARRIERTRAINER:_Glideslope(playerData) -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. local h=playerData.unit:GetAltitude()-self.deckheight - local x=math.abs(X-self.sterndist) + local x=math.abs(X-self.sterndist) --TODO: maybe sterndist should be replaced by position of 3-wire! local glideslope=math.atan(h/x) return math.deg(glideslope) @@ -1443,11 +1452,15 @@ function CARRIERTRAINER:_Debrief(playerData) end -- Send debrief message to player - self:_SendMessageToPlayer(text, 60, playerData, true) - - --TODO: add final grades, memorize score deductions. - --self:_PrintFinalScore(playerData, 30, -2) - --self:_HandleCollectedResult(playerData, -2) + self:_SendMessageToPlayer(text, 30, playerData, true) + + -- New approach. + if playerData.boltered or playerData.waveoff or playerData.patternwo then + local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) + local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) + local text=string.format("fly heading %d for %d NM to restart the pattern.", heading, UTILS.MetersToNM(distance)) + self:_SendMessageToPlayer(text, 10, playerData) + end -- Next step. playerData.step=0 @@ -1493,9 +1506,21 @@ function CARRIERTRAINER:_StepName(step) elseif step==7 then name="at the wake" elseif step==8 then - name="in the groove" - elseif step==9 then - name="trapped" + name="unkown" + elseif step==90 then + name="X0: Entering the Groove" + elseif step==91 then + name="X: At the Start" + elseif step==92 then + name="Roger Ball" + elseif step==93 then + name="IM: In the Middle" + elseif step==94 then + name="IC: In Close" + elseif step==95 then + name="AR: At the Ramp" + elseif step==96 then + name="IW: In the Wires" end return name @@ -1562,17 +1587,6 @@ function CARRIERTRAINER:_CheckAbort(X, Z, pos) abort=true end - --[[ - -- Abort conditions. - local abortXmin=check.Xmin and (check.Xmin<0 and X<=check.Xmin or check.Xmin>=0 and X>=check.Xmin) - local abortXmax=check.Xmax and (check.Xmax<0 and X>=check.Xmax or check.Xmax>=0 and X<=check.Xmax) - local abortZmin=check.Zmin and (check.Zmin<0 and Z<=check.Zmin or check.Zmin>=0 and Z>=check.Zmin) - local abortZmax=check.Zmax and (check.Zmax<0 and Z>=check.Zmax or check.Zmax>=0 and Z<=check.Zmax) - - -- Check if any of the conditions are met. - local abort=abortXmin or abortXmax or abortZmin or abortZmax - ]] - return abort end @@ -1665,13 +1679,20 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData) local relhead=self:_GetRelativeHeading(playerData.unit) - local text=string.format("%s, current step: %.1f\n", playerData.callsign, self:_StepName(playerData.step)) - text=text..string.format("AoA=%.1f | Vx=%.1f Vy=%.1f Vz=%.1f\n", aoa, velo.x, velo.y, velo.z) - text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) - text=text..string.format("pitch=%.1f | roll=%.1f | yaw=%.1f | climb=%.1f\n", pitch, roll, yaw, unit:GetClimbAngle()) - text=text..string.format("relheading=%.1f degrees\n", relhead) - text=text..string.format("Distance: X=%d m Z=%d m", dx, dz) - text=text..string.format("rho=%.1f m phi=%.1f degrees\n", rho,phi) + + local text=string.format("AoA=%.1f | Vx=%.1f Vy=%.1f Vz=%.1f\n", aoa, velo.x, velo.y, velo.z) + text=text..string.format("Pitch=%.1f° | Roll=%.1f° | Yaw=%.1f° | Climb=%.1f°\n", pitch, roll, yaw, unit:GetClimbAngle()) + text=text..string.format("Relheading=%.1f°\n", relhead) + text=text..string.format("Distance: X=%d m Z=%d m\n", dx, dz) + if playerData.step>=90 and playerData.step<=99 then + local lineup=self:_Lineup(playerData)-self.rwyangle + local glideslope=self:_Glideslope(playerData)-3.5 + text=text..string.format("Lineup Error = %.1f°\n", lineup) + text=text..string.format("Glideslope Error = %.1f°\n", glideslope) + end + text=text..string.format("Current step: %s\n", self:_StepName(playerData.step)) + --text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) + --text=text..string.format("rho=%.1f m phi=%.1f degrees\n", rho,phi) MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) end @@ -1711,7 +1732,7 @@ function CARRIERTRAINER:_InitStennis() -- Early break self.BreakEarly.name="Early Break" self.BreakEarly.Xmin=-500 - self.BreakEarly.Xmax=4000 + self.BreakEarly.Xmax=UTILS.NMToMeters(5) self.BreakEarly.Zmin=-3700 self.BreakEarly.Zmax=1500 self.BreakEarly.LimitXmin=0 @@ -1725,7 +1746,7 @@ function CARRIERTRAINER:_InitStennis() -- Late break self.BreakLate.name="Late Break" self.BreakLate.Xmin=-500 - self.BreakLate.Xmax=4000 + self.BreakLate.Xmax=UTILS.NMToMeters(5) self.BreakLate.Zmin=-3700 self.BreakLate.Zmax=1500 self.BreakLate.LimitXmin=0 @@ -1858,26 +1879,22 @@ function CARRIERTRAINER:_LSOgrade(playerData) elseif playerData.landed then --local gdata=playerData.groove.XX --#CARRIERTRAINER.GrooveData - - grade=grade..playerData.groove.XX grade=grade..playerData.groove.RB grade=grade..playerData.groove.IM grade=grade..playerData.groove.IC grade=grade..playerData.groove.AR - grade=grade..playerData.groove.IW - - + grade=grade..playerData.groove.IW else end end ---- Grade approach. +--- Grade flight data. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.GrooveData fdata Flight data in the groove. --- @return #string LSO grade. +-- @return #string LSO grade or empty string if flight data table is nil. function CARRIERTRAINER:_Flightdata2Text(fdata) local function little(text) @@ -1887,18 +1904,19 @@ function CARRIERTRAINER:_Flightdata2Text(fdata) return string.format("_%s_", text) end + -- No flight data ==> return empty string. if fdata==nil then return "" end + -- Flight data. local step=fdata.Step local AOA=fdata.AoA local GSE=fdata.GSE local LUE=fdata.LUE local ROL=fdata.Roll - - local Y=self:_GS(step) + -- Speed. local S=nil if AOA>9.3 then S=underline("F") @@ -1914,6 +1932,7 @@ function CARRIERTRAINER:_Flightdata2Text(fdata) S=little("SLO") end + -- Alitude. local A=nil if GSE>1 then A=underline("H") @@ -1929,6 +1948,7 @@ function CARRIERTRAINER:_Flightdata2Text(fdata) A="LO" end + -- Line up. local D=nil if LUE>3 then D=underline("LUL") @@ -1944,6 +1964,7 @@ function CARRIERTRAINER:_Flightdata2Text(fdata) D=little("LUL") end + -- Compile. local G="" if S then G=G..S @@ -1955,8 +1976,9 @@ function CARRIERTRAINER:_Flightdata2Text(fdata) G=G..D end + -- Add current step. if G~="" then - G=G..Y + G=G..self:_GS(step) end return G @@ -1991,44 +2013,44 @@ end -- @param #CARRIERTRAINER.Checkpoint checkpoint Checkpoint. -- @param #number altitude Player's current altitude in meters. -- @return #string Feedback text. +-- @return #string Debriefing text. function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint, altitude) -- Player altitude. local altitude=playerData.unit:GetAltitude() -- Get relative score. - local lowscore, badscore = self:_GetGoodBadScore(playerData) + 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) + hint=string.format("You're high. ") elseif _error>lowscore then - score = -5 - hint = string.format("You're slightly high %s. ", steptext) + hint= string.format("You're slightly high. ") elseif _error<-badscore then - score = -10 - hint = string.format("You're low %s.", steptext) + hint=string.format("You're low. ") elseif _error<-lowscore then - score = -5 - hint = string.format("You're slightly low %s. ", steptext) + hint=string.format("You're slightly low. ") else - score = 0 - hint = string.format("Good altitude %s. ", steptext) + hint=string.format("Good altitude. ") end - hint=hint..string.format(" Altitude %d ft = %d%% deviation from %d ft target alt.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(checkpoint.Altitude)) + -- Extend or decrease depending on skill. + if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then + hint=hint..string.format("Optimal altitude is %d ft.\n", UTILS.MetersToFeet(checkpoint.Altitude)) + elseif playerData.difficulty==CARRIERTRAINER.Difficulty.NORMAL then + hint=hint.."\n" + elseif playerData.difficulty==CARRIERTRAINER.Difficulty.HARD then + hint="" + end - -- Set score. - playerData.score=playerData.score+score - - return hint + -- Debrief text. + local debrief=string.format("Altitude %d ft = %d%% deviation from %d ft optimum.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(checkpoint.Altitude)) + + return hint, debrief end --- Evaluate player's altitude at checkpoint. @@ -2037,6 +2059,7 @@ end -- @param #CARRIERTRAINER.Checkpoint checkpoint Checkpoint. -- @param #number distance Player's current distance to the boat in meters. -- @return #string Feedback message text. +-- @return #string Debriefing text. function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance) -- Get relative score. @@ -2045,32 +2068,33 @@ function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance) -- 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.") + hint=string.format("You're too far from the boat! ") + elseif _error>lowscore then + 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!") + hint=string.format( "You're too close to the boat! ") elseif _error<-lowscore then - score = -5 - hint = string.format("You're slightly too far from the boat.") + hint=string.format("You're slightly too far from the boat. ") else - score = 0 - hint = string.format("Perfect distance to the boat.") + hint=string.format("Perfect distance to the boat. ") end - hint=hint..string.format(" Distance %.1f NM = %d%% deviation from %.1f NM optimal distance.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(checkpoint.Distance)) + -- Extend or decrease depending on skill. + if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then + hint=hint..string.format(" Optimal distance is %d NM.", UTILS.MetersToNM(checkpoint.Distance)) + elseif playerData.difficulty==CARRIERTRAINER.Difficulty.NORMAL then + hint=hint.."\n" + elseif playerData.difficulty==CARRIERTRAINER.Difficulty.HARD then + hint="" + end - -- Set score. - playerData.score=playerData.score+score - - return hint + -- Debriefing text. + local debrief=string.format("Distance %.1f NM = %d%% deviation from %.1f NM optimum.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(checkpoint.Distance)) + + return hint, debrief end --- Score for correct AoA. @@ -2078,7 +2102,8 @@ end -- @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. +-- @return #string Feedback message text or easy and normal difficulty level or nil for hard. +-- @return #string Debriefing text. function CARRIERTRAINER:_AoACheck(playerData, checkpoint, aoa) -- Get relative score. @@ -2087,31 +2112,32 @@ function CARRIERTRAINER:_AoACheck(playerData, checkpoint, aoa) -- Altitude error +-X% local _error=(aoa-checkpoint.AoA)/checkpoint.AoA*100 - local score = 0 - local hint="" + local hint if _error>badscore then --Slow - score = -10 - hint = "You're slow." + hint="You're slow. " elseif _error>lowscore then --Slightly slow - score = -5 - hint = "You're slightly slow." + hint="You're slightly slow. " elseif _error<-badscore then --Fast - score = -10 - hint = "You're fast." + hint="You're fast. " elseif _error<-lowscore then --Slightly fast - score = -5 - hint = "You're slightly fast." + hint="You're slightly fast. " else --On speed - score = 0 - hint = "You're on speed." + hint="You're on speed. " + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then + hint=hint..string.format(" Optimal AoA is %.1f.", checkpoint.AoA) + elseif playerData.difficulty==CARRIERTRAINER.Difficulty.NORMAL then + hint=hint.."\n" + elseif playerData.difficulty==CARRIERTRAINER.Difficulty.HARD then + hint="" end - hint=hint..string.format(" AoA %.1f = %d %% deviation from %.1f target AoA.", aoa, _error, checkpoint.AoA) - - -- Set score. - playerData.score=playerData.score+score + -- Debriefing text. + local debrief=string.format("AoA %.1f = %d%% deviation from %.1f optimum.", aoa, _error, checkpoint.AoA) - return hint + return hint, debrief end @@ -2122,72 +2148,16 @@ end -- @param #CARRIERTRAINER.PlayerData playerData Player data. -- @param #boolean clear If true, clear screen from previous messages. function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData, clear) - if playerData.client then - --MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToClient(playerData.client) + if message then + if playerData.client then + --MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToClient(playerData.client) + end + local text=string.format("%s, %s, %s", self.alias, playerData.callsign, message) + MESSAGE:New(text, duration, nil, clear):ToAll() + env.info(text) end - local text=string.format("%s, %s, %s", self.alias, playerData.callsign, message) - MESSAGE:New(text, duration, nil, clear):ToAll() - env.info(text) 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 - - --- 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. @@ -2278,6 +2248,8 @@ function CARRIERTRAINER:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "Carrier Info", _trainPath, self._DisplayCarrierInfo, self, _unitName) missionCommands.addCommandForGroup(_gid, "Weather Report", _trainPath, self._DisplayCarrierWeather, self, _unitName) --TODO: Flare carrier. + + missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _trainPath, self._AttitudeMonitor, self, playername) end else self:T(self.lid.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) @@ -2327,7 +2299,7 @@ function CARRIERTRAINER:_DisplayScoreBoard(_unitName) end --Sort list! - local _sort = function( a,b ) return a.hits > b.hits end + local _sort=function(a, b) return a.hits>b.hits end table.sort(_playerResults,_sort) -- Add top 10 results. @@ -2346,14 +2318,27 @@ function CARRIERTRAINER:_DisplayScoreBoard(_unitName) end +--- Turn player's aircraft attitude display on or off. +-- @param #CARRIERTRAINER self +-- @param #string playername Player name. +function CARRIERTRAINER:_AttitudeMonitor(playername) + self:E({playername=playername}) + + local playerData=self.players[playername] --#CARRIERTRAINER.PlayerData + + if playerData then + playerData.attitudemonitor=not playerData.attitudemonitor + end +end + --- Set difficulty level. -- @param #CARRIERTRAINER self --- @param #string playernaame Player name. +-- @param #string playername Player name. -- @param #CARRIERTRAINER.Difficulty difficulty Difficulty level. function CARRIERTRAINER:_SetDifficulty(playername, difficulty) self:E({difficulty=difficulty, playername=playername}) - local playerData=self.players[playername] --CARRIERTRAINER.PlayerData + local playerData=self.players[playername] --#CARRIERTRAINER.PlayerData if playerData then playerData.difficulty=difficulty @@ -2479,27 +2464,3 @@ function CARRIERTRAINER:_DisplayCarrierWeather(_unitname) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ --- LSO Class ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - ---- LSO class. --- @type LSO --- @field #string ClassName Name of the class. --- @extends Core.Fsm#FSM - ---- Landing Signal Officer --- --- === --- --- ![Banner Image](..\Presentations\LSO\LSO_Main.png) --- --- # The Landing Signal Officer --- --- bla bla --- --- @field #LSO -LSO = { - ClassName = "LSO", -} - From 6d45c09ea28962677be8b760ff0b1cb3945279fa Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 6 Nov 2018 16:04:19 +0100 Subject: [PATCH 015/147] CTv0.18w --- .../Moose/Functional/CarrierTrainer.lua | 79 +++++++++++++------ 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index a17c30d0a..12cf8d8df 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -81,6 +81,8 @@ CARRIERTRAINER = { rwyangle = -10, sterndist =-100, deckheight = 22, + Qpattern = {}, + Qmarshal = {}, } --- Aircraft types. @@ -105,6 +107,25 @@ CARRIERTRAINER.CarrierType={ KUZNETSOV="KUZNECOW" } +--- Pattern steps. +-- @type CARRIERTRAINER.PatternStep +CARRIERTRAINER.PatternStep={ + UNREGISTERED="Unregistered", + PATTERNENTRY="Pattern Entry", + EARLYBREAK="Early Break", + LATEBREAK="Late Break", + ABEAM="Abeam", + NINETY="Ninety", + WAKE="Wake", + GROOVE_X0="Groove Entry", + GROOVE_XX="Groove X", + GROOVE_RB="Groove Roger Ball", + GROOVE_IM="Groove In the Middle", + GROOVE_IC="Groove In Close", + GROOVE_AR="Groove At the Ramp", + GROOVE_IW="Groove In the Wires", +} + --- LSO calls. -- @type CARRIERTRAINER.LSOcall -- @field Core.UserSound#USERSOUND RIGHTFORLINEUPL "Right for line up!" call (loud). @@ -207,7 +228,7 @@ CARRIERTRAINER.GroovePos={ -- @field #string difficulty Difficulty level. -- @field #number score Player score of the current pass. -- @field #number passes Number of passes. --- @field #boolan attitudemonitor If true, display aircraft attitude and other parameters constantly. +-- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. -- @field #table debrief Debrief analysis of the current step of this pass. -- @field #table grade LSO grade of passes. -- @field #boolean inbigzone If true, player is in the big zone. @@ -1448,7 +1469,7 @@ function CARRIERTRAINER:_Debrief(playerData) local comment=_data.hint text=text..string.format("* %s:\n",step) text=text..string.format("%s\n", comment) - text=text..string.format("------------------------------------------------------------\n") + --text=text..string.format("------------------------------------------------------------\n") end -- Send debrief message to player @@ -1492,35 +1513,35 @@ function CARRIERTRAINER:_StepName(step) if step==0 then name="Unregistered" elseif step==1 then - name="when entering pattern" + name="Pattern Entry" elseif step==2 then - name="in the break entry" + name="Break Entry" elseif step==3 then - name="at the early break" + name="Early break" elseif step==4 then - name="at the late break" + name="Late break" elseif step==5 then - name="in the abeam position" + name="Abeam position" elseif step==6 then - name="at the ninety" + name="Ninety" elseif step==7 then - name="at the wake" + name="Wake" elseif step==8 then name="unkown" elseif step==90 then - name="X0: Entering the Groove" + name="Entering the Groove" elseif step==91 then - name="X: At the Start" + name="Groove: X At the Start" elseif step==92 then - name="Roger Ball" + name="Groove: Roger Ball" elseif step==93 then - name="IM: In the Middle" + name="Groove: IM In the Middle" elseif step==94 then - name="IC: In Close" + name="Groove: IC In Close" elseif step==95 then - name="AR: At the Ramp" + name="Groove: AR: At the Ramp" elseif step==96 then - name="IW: In the Wires" + name="Groove: IW: In the Wires" end return name @@ -1601,9 +1622,9 @@ function CARRIERTRAINER:_TooFarOutText(X, Z, posData) local xtext=nil if posData.Xmin and XposData.Xmax then - xtext=" behind" + xtext="behind" end local ztext=nil @@ -1614,7 +1635,7 @@ function CARRIERTRAINER:_TooFarOutText(X, Z, posData) end if xtext and ztext then - text=text..xtext.." and"..ztext + text=text..xtext.." and "..ztext elseif xtext then text=text..xtext elseif ztext then @@ -1638,7 +1659,7 @@ function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData) local toofartext=self:_TooFarOutText(X, Z, posData) -- Send message to player. - self:_SendMessageToPlayer(toofartext.." Depart and reenter!", 15, playerData, true) + self:_SendMessageToPlayer(toofartext.." Depart and re-enter!", 15, playerData, true) -- 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)) @@ -1646,7 +1667,7 @@ function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData) --MESSAGE:New(text, 60):ToAllIf(self.Debug) -- Add to debrief. - self:_AddToSummary(playerData, "Abort", "Approach aborted.") + self:_AddToSummary(playerData, string.format("Pattern Wave Off (%s)", self:_StepName(playerData.step)), string.format()) -- Pattern wave off! playerData.patternwo=true @@ -1663,27 +1684,33 @@ end -- @param #CARRIERTRAINER.PlayerData playerData Player data. function CARRIERTRAINER:_DetailedPlayerStatus(playerData) + -- Player unit. local unit=playerData.unit + -- Aircraft attitude. local aoa=unit:GetAoA() local yaw=unit:GetYaw() local roll=unit:GetRoll() local pitch=unit:GetPitch() + + -- Distance to the boat. local dist=playerData.unit:GetCoordinate():Get2DDistance(self.carrier:GetCoordinate()) local dx,dz,rho,phi=self:_GetDistances(unit) - local heading=unit:GetCoordinate():HeadingTo(self.startZone:GetCoordinate()) - + -- Wind vector. local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() + + -- Aircraft veloecity vector. local velo=unit:GetVelocityVec3() + -- Relative heading Aircraft to Carrier. local relhead=self:_GetRelativeHeading(playerData.unit) - + -- Output local text=string.format("AoA=%.1f | Vx=%.1f Vy=%.1f Vz=%.1f\n", aoa, velo.x, velo.y, velo.z) text=text..string.format("Pitch=%.1f° | Roll=%.1f° | Yaw=%.1f° | Climb=%.1f°\n", pitch, roll, yaw, unit:GetClimbAngle()) text=text..string.format("Relheading=%.1f°\n", relhead) - text=text..string.format("Distance: X=%d m Z=%d m\n", dx, dz) + text=text..string.format("Distance: X=%d m Z=%d m | R=%d m Phi=%.1f\n", dx, dz, rho, phi) if playerData.step>=90 and playerData.step<=99 then local lineup=self:_Lineup(playerData)-self.rwyangle local glideslope=self:_Glideslope(playerData)-3.5 @@ -1691,6 +1718,7 @@ function CARRIERTRAINER:_DetailedPlayerStatus(playerData) text=text..string.format("Glideslope Error = %.1f°\n", glideslope) end text=text..string.format("Current step: %s\n", self:_StepName(playerData.step)) + --text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) --text=text..string.format("rho=%.1f m phi=%.1f degrees\n", rho,phi) @@ -2069,7 +2097,6 @@ function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance) local _error=(distance-checkpoint.Distance)/checkpoint.Distance*100 local hint - local steptext=self:_StepName(playerData.step) if _error>badscore then hint=string.format("You're too far from the boat! ") elseif _error>lowscore then From 708ff794b01e40c791d6b69a70e2bce784144fea Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 7 Nov 2018 00:37:49 +0100 Subject: [PATCH 016/147] CT v0.1.9 --- .../Moose/Functional/CarrierTrainer.lua | 180 +++++++++++++----- 1 file changed, 134 insertions(+), 46 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 12cf8d8df..810f8eb87 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -215,10 +215,10 @@ CARRIERTRAINER.GroovePos={ -- @field #number Roll Roll angle. --- LSO grade --- @type CARRIERDATA.LSOgrade --- @field #string grade LSO grade string --- @field #string details Detailed step analysis. +-- @type CARRIERTRAINER.LSOgrade +-- @field #string grade LSO grade, i.e. _OK_, OK, (OK), --, CUT -- @field #number points Points received. +-- @field #string details Detailed flight analyis analysis. --- Player data table holding all important parameters of each player. -- @type CARRIERTRAINER.PlayerData @@ -236,7 +236,8 @@ CARRIERTRAINER.GroovePos={ -- @field #boolean bolter If true, LSO told player to bolter. -- @field #boolean boltered If true, player boltered. -- @field #boolean waveoff If true, player was waved off during final approach. --- @field #boolean patternwo If true, playe was waved of during the pattern. +-- @field #boolean patternwo If true, player was waved of during the pattern. +-- @field #boolean lig If true, player was long in the groove. -- @field #number Tlso Last time the LSO gave an advice. -- @field #CARRIERTRAINER.GroovePos groove Data table at each position in the groove. Elemets are of type @{#CARRIERTRAINER.GrooveData}. @@ -263,7 +264,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.8" +CARRIERTRAINER.version="0.1.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -500,7 +501,7 @@ function CARRIERTRAINER:_CheckPlayerStatus() self:_CallTheBall(playerData) elseif playerData.step==999 then -- Debriefing. - SCHEDULER:New(nil, self._Debrief, {self,playerData}, 10) + SCHEDULER:New(nil, self._Debrief, {self, playerData}, 10) playerData.step=-1 end @@ -565,7 +566,7 @@ function CARRIERTRAINER:OnEventBirth(EventData) self.players[_playername]=self:_InitPlayer(_unitName) -- Start in the groove for debugging. - self.groovedebug=false + self.groovedebug=true end end @@ -670,7 +671,8 @@ function CARRIERTRAINER:_InitNewRound(playerData) playerData.score=100 playerData.groove={} playerData.debrief={} - playerData.patternwo=false + playerData.patternwo=false + playerData.lig=false playerData.waveoff=false playerData.bolter=false playerData.boltered=false @@ -814,7 +816,7 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) local relhead=self:_GetRelativeHeading(playerData.unit) -- One NM from carrier is too far. - local limit=-UTILS.NMToMeters(1.5) + local limit=UTILS.NMToMeters(-1.5) local text=string.format("Long groove check: X=%d, relhead=%.1f", X, relhead) self:T(text) @@ -832,7 +834,8 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) -- Debrief. self:_AddToSummary(playerData, "Downwind", "Long in the groove.") - local grade="LIG PATTERN WAVE OFF - CUT 1 PT" + --grade="LIG PATTERN WAVE OFF - CUT 1 PT" + playerData.lig=true -- Next step: Debriefing. playerData.step=999 @@ -1143,6 +1146,8 @@ function CARRIERTRAINER:_CallTheBall(playerData) CARRIERTRAINER.LSOcall.WAVEOFF:ToGroup(playerData.unit:GetGroup()) playerData.Tlso=timer.getTime() + playerData.waveoff=true + -- Next step: debrief. playerData.step=999 @@ -1405,7 +1410,7 @@ function CARRIERTRAINER:_Glideslope(playerData) -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. local h=playerData.unit:GetAltitude()-self.deckheight - local x=math.abs(X-self.sterndist) --TODO: maybe sterndist should be replaced by position of 3-wire! + local x=math.abs(-86-X) --math.abs(self.sterndist-X) --TODO: maybe sterndist should be replaced by position of 3-wire! local glideslope=math.atan(h/x) return math.deg(glideslope) @@ -1473,14 +1478,23 @@ function CARRIERTRAINER:_Debrief(playerData) end -- Send debrief message to player - self:_SendMessageToPlayer(text, 30, playerData, true) + self:_SendMessageToPlayer(text, 30, playerData, true, "Paddles") + + -- LSO grade, points, and flight data analyis. + local grade, points, analysis=self:_LSOgrade(playerData) + + -- LSO grade message. + text=string.format("%s %.1f PT - %s", grade, points, analysis) + self:_SendMessageToPlayer(text, 30, playerData, true, "Paddles", 30) + + --TODO: Add grade to table. -- New approach. if playerData.boltered or playerData.waveoff or playerData.patternwo then local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) local text=string.format("fly heading %d for %d NM to restart the pattern.", heading, UTILS.MetersToNM(distance)) - self:_SendMessageToPlayer(text, 10, playerData) + self:_SendMessageToPlayer(text, 10, playerData, false, nil, 20) end -- Next step. @@ -1667,7 +1681,7 @@ function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData) --MESSAGE:New(text, 60):ToAllIf(self.Debug) -- Add to debrief. - self:_AddToSummary(playerData, string.format("Pattern Wave Off (%s)", self:_StepName(playerData.step)), string.format()) + self:_AddToSummary(playerData, string.format("%s", self:_StepName(playerData.step)), string.format("Pattern wave off: %s", toofartext)) -- Pattern wave off! playerData.patternwo=true @@ -1891,38 +1905,88 @@ end --- Grade approach. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. --- @return #string LSO grade. +-- @return #string LSO grade, i.g. _OK_, OK, (OK), --, etc. +-- @return #number Points. +-- @return #string LSO analysis of flight path. function CARRIERTRAINER:_LSOgrade(playerData) - - local grade="" - if playerData.patternwo then - return + local function count(base, pattern) + return select(2, string.gsub(base, pattern, "")) end - if playerData.waveoff then + -- Analyse flight data and conver to LSO text. + local GXX,nXX=self:_Flightdata2Text(playerData.groove.XX) + local GIM,nIM=self:_Flightdata2Text(playerData.groove.IM) + local GIC,nIC=self:_Flightdata2Text(playerData.groove.IC) + local GAR,nAR=self:_Flightdata2Text(playerData.groove.AR) - elseif playerData.boltered then + -- Put everything together. + local G=GXX.." "..GIM.." ".." "..GIC.." "..GAR - elseif playerData.landed then + -- Ground number of minor, normal and major deviations. + local N=nXX+nIM+nIC+nAR + local nL=count(G, '_')/2 + local nS=count(G, '%(') + local nN=N-nS-nL - --local gdata=playerData.groove.XX --#CARRIERTRAINER.GrooveData - grade=grade..playerData.groove.XX - grade=grade..playerData.groove.RB - grade=grade..playerData.groove.IM - grade=grade..playerData.groove.IC - grade=grade..playerData.groove.AR - grade=grade..playerData.groove.IW + local grade + local points + if N==0 then + -- No deviations, should be REALLY RARE! + grade="_OK_" + points=5.0 else + if nL>0 then + -- Larger deviations ==> "No grade" 2.0 points. + grade="--" + points=2.0 + elseif nN>0 then + -- No larger but average deviations ==> "Fair Pass" Pass with average deviations and corrections. + grade="(OK)" + points=3.0 + else + -- Only minor corrections + grade="OK" + points=4.0 + end + end + env.info("LSO grade:") + env.info(G) + G=G:gsub("%)%(", "") + G=G:gsub("__","") + env.info(G) + env.info("Grade = "..grade.." points = "..points) + env.info("# of total deviations = "..N) + env.info("# of large deviations _ = "..nL) + env.info("# of norma deviations _ = "..nN) + env.info("# of small deviations ( = "..nS) + env.info() + + if playerData.patternwo or playerData.waveoff then + grade="CUT" + points=1.0 + if playerData.lig then + G="LIG PWO" + elseif playerData.patternwo then + G="PWO "..G + end + if playerData.landed then + --AIRBOSS wants to talk to you! + end + elseif playerData.boltered then + grade="-- (BOLTER)" + points=2.5 end + return grade, points, G end --- Grade flight data. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.GrooveData fdata Flight data in the groove. -- @return #string LSO grade or empty string if flight data table is nil. +-- @return #number Number of deviations from perfect flight path. function CARRIERTRAINER:_Flightdata2Text(fdata) local function little(text) @@ -1934,7 +1998,8 @@ function CARRIERTRAINER:_Flightdata2Text(fdata) -- No flight data ==> return empty string. if fdata==nil then - return "" + env.info("FF fdata nil") + return "", 0 end -- Flight data. @@ -1945,19 +2010,19 @@ function CARRIERTRAINER:_Flightdata2Text(fdata) local ROL=fdata.Roll -- Speed. - local S=nil - if AOA>9.3 then - S=underline("F") - elseif AOA>8.7 then - S="F" - elseif AOA>8.3 then - S=little("F") - elseif AOA<6.7 then + local S=nil + if AOA>9.8 then S=underline("SLO") - elseif AOA<7.7 then + elseif AOA>9.3 then S="SLO" - elseif AOA<7.9 then + elseif AOA>8.8 then S=little("SLO") + elseif AOA<6.4 then + S=underline("F") + elseif AOA<6.9 then + S="F" + elseif AOA<7.4 then + S=little("F") end -- Alitude. @@ -1989,19 +2054,23 @@ function CARRIERTRAINER:_Flightdata2Text(fdata) elseif LUE<-1 then D="LUR" elseif LUE<-0.5 then - D=little("LUL") + D=little("LUR") end -- Compile. local G="" + local n=0 if S then G=G..S - end + n=n+1 + end if A then G=G..A + n=n+1 end if D then G=G..D + n=n+1 end -- Add current step. @@ -2009,7 +2078,13 @@ function CARRIERTRAINER:_Flightdata2Text(fdata) G=G..self:_GS(step) end - return G + env.info(string.format("AOA=%.1f",AOA)) + env.info(string.format("GSE=%.1f",GSE)) + env.info(string.format("LUE=%.1f",LUE)) + env.info(string.format("ROL=%.1f",ROL)) + env.info(G) + + return G,n end --- Evaluate player's altitude at checkpoint. @@ -2174,14 +2249,27 @@ end -- @param #number duration Display message duration. -- @param #CARRIERTRAINER.PlayerData playerData Player data. -- @param #boolean clear If true, clear screen from previous messages. -function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData, clear) +-- @param #string sender The person who sends the message. Default is carrier alias. +-- @param #number delay Delay in seconds, before the message is send. +function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData, clear, sender, delay) if message then + + delay=delay or 0 + sender=sender or self.alias + if playerData.client then --MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToClient(playerData.client) end - local text=string.format("%s, %s, %s", self.alias, playerData.callsign, message) - MESSAGE:New(text, duration, nil, clear):ToAll() + + local text=string.format("%s, %s, %s", sender, playerData.callsign, message) env.info(text) + + if delay>0 then + SCHEDULER:New(nil,self._SendMessageToPlayer, {self, message, duration, playerData, clear, sender}, delay) + else + MESSAGE:New(text, duration, nil, clear):ToAll() + end + end end From 0e656c8520bacf4019f83b9661760a952b64bef5 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 7 Nov 2018 07:24:45 +0100 Subject: [PATCH 017/147] Progress, patrolling works. Engagement not yet. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 623 +++++++++--------- Moose Development/Moose/AI/AI_A2G_Patrol.lua | 488 ++++++++++++++ Moose Setup/Moose.files | 1 + 3 files changed, 794 insertions(+), 318 deletions(-) create mode 100644 Moose Development/Moose/AI/AI_A2G_Patrol.lua diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 03aa7cd9b..3eacf59fb 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -281,8 +281,8 @@ do -- AI_A2G_DISPATCHER self:SetDefaultGrouping( 1 ) self:SetDefaultFuelThreshold( 0.15, 0 ) -- 15% of fuel remaining in the tank will trigger the airplane to return to base or refuel. self:SetDefaultDamageThreshold( 0.4 ) -- When 40% of damage, go RTB. - self:SetDefaultCapTimeInterval( 180, 600 ) -- Between 180 and 600 seconds. - self:SetDefaultCapLimit( 1 ) -- Maximum one CAP per squadron. + self:SetDefaultPatrolTimeInterval( 180, 600 ) -- Between 180 and 600 seconds. + self:SetDefaultPatrolLimit( 1 ) -- Maximum one Patrol per squadron. self:AddTransition( "Started", "Assign", "Started" ) @@ -297,33 +297,33 @@ do -- AI_A2G_DISPATCHER -- @param Wrapper.Unit#UNIT TaskUnit -- @param #string PlayerName - self:AddTransition( "*", "CAP", "*" ) + self:AddTransition( "*", "Patrol", "*" ) - --- CAP Handler OnBefore for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeCAP + --- Patrol Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforePatrol -- @param #AI_A2G_DISPATCHER self -- @param #string From -- @param #string Event -- @param #string To -- @return #boolean - --- CAP Handler OnAfter for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] OnAfterCAP + --- Patrol Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterPatrol -- @param #AI_A2G_DISPATCHER self -- @param #string From -- @param #string Event -- @param #string To - --- CAP Trigger for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] CAP + --- Patrol Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Patrol -- @param #AI_A2G_DISPATCHER self - --- CAP Asynchronous Trigger for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] __CAP + --- Patrol Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Patrol -- @param #AI_A2G_DISPATCHER self -- @param #number Delay - self:AddTransition( "*", "DEFEND", "*" ) + self:AddTransition( "*", "Defend", "*" ) --- GCI Handler OnBefore for AI_A2G_DISPATCHER -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeGCI @@ -395,7 +395,7 @@ do -- AI_A2G_DISPATCHER self:SetTacticalDisplay( false ) - self.DefenderCAPIndex = 0 + self.DefenderPatrolIndex = 0 self:__Start( 5 ) @@ -626,8 +626,8 @@ do -- AI_A2G_DISPATCHER --- Define a border area to simulate a **cold war** scenario. - -- A **cold war** is one where CAP aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. - -- A **hot war** is one where CAP aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send CAP and GCI aircraft to attack it. + -- A **cold war** is one where Patrol aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. + -- A **hot war** is one where Patrol aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send Patrol and GCI aircraft to attack it. -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this. -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1 -- @param #AI_A2G_DISPATCHER self @@ -704,45 +704,45 @@ do -- AI_A2G_DISPATCHER end - --- Set the default CAP time interval for squadrons, which will be used to determine a random CAP timing. - -- The default CAP time interval is between 180 and 600 seconds. + --- Set the default Patrol time interval for squadrons, which will be used to determine a random Patrol timing. + -- The default Patrol time interval is between 180 and 600 seconds. -- @param #AI_A2G_DISPATCHER self - -- @param #number CapMinSeconds The minimum amount of seconds for the random time interval. - -- @param #number CapMaxSeconds The maximum amount of seconds for the random time interval. + -- @param #number PatrolMinSeconds The minimum amount of seconds for the random time interval. + -- @param #number PatrolMaxSeconds The maximum amount of seconds for the random time interval. -- @return #AI_A2G_DISPATCHER -- @usage -- -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) -- - -- -- Now Setup the default CAP time interval. - -- A2GDispatcher:SetDefaultCapTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. + -- -- Now Setup the default Patrol time interval. + -- A2GDispatcher:SetDefaultPatrolTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. -- - function AI_A2G_DISPATCHER:SetDefaultCapTimeInterval( CapMinSeconds, CapMaxSeconds ) + function AI_A2G_DISPATCHER:SetDefaultPatrolTimeInterval( PatrolMinSeconds, PatrolMaxSeconds ) - self.DefenderDefault.CapMinSeconds = CapMinSeconds - self.DefenderDefault.CapMaxSeconds = CapMaxSeconds + self.DefenderDefault.PatrolMinSeconds = PatrolMinSeconds + self.DefenderDefault.PatrolMaxSeconds = PatrolMaxSeconds return self end - --- Set the default CAP limit for squadrons, which will be used to determine how many CAP can be airborne at the same time for the squadron. - -- The default CAP limit is 1 CAP, which means one CAP group being spawned. + --- Set the default Patrol limit for squadrons, which will be used to determine how many Patrol can be airborne at the same time for the squadron. + -- The default Patrol limit is 1 Patrol, which means one Patrol group being spawned. -- @param #AI_A2G_DISPATCHER self - -- @param #number CapLimit The maximum amount of CAP that can be airborne at the same time for the squadron. + -- @param #number PatrolLimit The maximum amount of Patrol that can be airborne at the same time for the squadron. -- @return #AI_A2G_DISPATCHER -- @usage -- -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) -- - -- -- Now Setup the default CAP limit. - -- A2GDispatcher:SetDefaultCapLimit( 2 ) -- Maximum 2 CAP per squadron. + -- -- Now Setup the default Patrol limit. + -- A2GDispatcher:SetDefaultPatrolLimit( 2 ) -- Maximum 2 Patrol per squadron. -- - function AI_A2G_DISPATCHER:SetDefaultCapLimit( CapLimit ) + function AI_A2G_DISPATCHER:SetDefaultPatrolLimit( PatrolLimit ) - self.DefenderDefault.CapLimit = CapLimit + self.DefenderDefault.PatrolLimit = PatrolLimit return self end @@ -1046,115 +1046,45 @@ do -- AI_A2G_DISPATCHER end - --- Set a CAP for a Squadron. + + --- Set the squadron Patrol parameters. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed. - -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. - -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. - -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. - -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. - -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. - -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- CAP Squadron execution. - -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) - -- A2GDispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- A2GDispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) - -- - -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) - -- A2GDispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) - -- A2GDispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") - -- A2GDispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) - -- A2GDispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - function AI_A2G_DISPATCHER:SetSquadronCap( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - local Cap = self.DefenderSquadrons[SquadronName].Cap - Cap.Name = SquadronName - Cap.Zone = Zone - Cap.FloorAltitude = FloorAltitude - Cap.CeilingAltitude = CeilingAltitude - Cap.PatrolMinSpeed = PatrolMinSpeed - Cap.PatrolMaxSpeed = PatrolMaxSpeed - Cap.EngageMinSpeed = EngageMinSpeed - Cap.EngageMaxSpeed = EngageMaxSpeed - Cap.AltType = AltType - - self:SetSquadronCapInterval( SquadronName, self.DefenderDefault.CapLimit, self.DefenderDefault.CapMinSeconds, self.DefenderDefault.CapMaxSeconds, 1 ) - - self:F( { CAP = { SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType } } ) - - -- Add the CAP to the EWR network. - - local RecceSet = self.Detection:GetDetectionSetGroup() - RecceSet:FilterPrefixes( DefenderSquadron.TemplatePrefixes ) - RecceSet:FilterStart() - - self.Detection:SetFriendlyPrefixes( DefenderSquadron.TemplatePrefixes ) - - return self - end - - --- Set the squadron CAP parameters. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number CapLimit (optional) The maximum amount of CAP groups to be spawned. Note that a CAP is a group, so can consist out of 1 to 4 airplanes. The default is 1 CAP group. - -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new CAP will be spawned. The default is 180 seconds. - -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new CAP will be spawned. The default is 600 seconds. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. -- @param #number Probability Is not in use, you can skip this parameter. -- @return #AI_A2G_DISPATCHER -- @usage -- - -- -- CAP Squadron execution. - -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) - -- A2GDispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- A2GDispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) - -- - -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) - -- A2GDispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) - -- A2GDispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") - -- A2GDispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) - -- A2GDispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) -- - function AI_A2G_DISPATCHER:SetSquadronCapInterval( SquadronName, CapLimit, LowInterval, HighInterval, Probability ) + function AI_A2G_DISPATCHER:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, DefenseTaskType ) - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} - local DefenderSquadron = self:GetSquadron( SquadronName ) - local Cap = self.DefenderSquadrons[SquadronName].Cap - if Cap then - Cap.LowInterval = LowInterval or 180 - Cap.HighInterval = HighInterval or 600 - Cap.Probability = Probability or 1 - Cap.CapLimit = CapLimit or 1 - Cap.Scheduler = Cap.Scheduler or SCHEDULER:New( self ) - local Scheduler = Cap.Scheduler -- Core.Scheduler#SCHEDULER - local ScheduleID = Cap.ScheduleID - local Variance = ( Cap.HighInterval - Cap.LowInterval ) / 2 - local Repeat = Cap.LowInterval + Variance + local Patrol = DefenderSquadron[DefenseTaskType] + if Patrol then + Patrol.LowInterval = LowInterval or 180 + Patrol.HighInterval = HighInterval or 600 + Patrol.Probability = Probability or 1 + Patrol.PatrolLimit = PatrolLimit or 1 + Patrol.Scheduler = Patrol.Scheduler or SCHEDULER:New( self ) + local Scheduler = Patrol.Scheduler -- Core.Scheduler#SCHEDULER + local ScheduleID = Patrol.ScheduleID + local Variance = ( Patrol.HighInterval - Patrol.LowInterval ) / 2 + local Repeat = Patrol.LowInterval + Variance local Randomization = Variance / Repeat - local Start = math.random( 1, Cap.HighInterval ) + local Start = math.random( 1, Patrol.HighInterval ) if ScheduleID then Scheduler:Stop( ScheduleID ) end - Cap.ScheduleID = Scheduler:Schedule( self, self.SchedulerCAP, { SquadronName }, Start, Repeat, Randomization ) + Patrol.ScheduleID = Scheduler:Schedule( self, self.SchedulerPatrol, { SquadronName }, Start, Repeat, Randomization ) else error( "This squadron does not exist:" .. SquadronName ) end @@ -1165,16 +1095,16 @@ do -- AI_A2G_DISPATCHER -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:GetCAPDelay( SquadronName ) + function AI_A2G_DISPATCHER:GetPatrolDelay( SquadronName ) self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} + self.DefenderSquadrons[SquadronName].Patrol = self.DefenderSquadrons[SquadronName].Patrol or {} local DefenderSquadron = self:GetSquadron( SquadronName ) - local Cap = self.DefenderSquadrons[SquadronName].Cap - if Cap then - return math.random( Cap.LowInterval, Cap.HighInterval ) + local Patrol = self.DefenderSquadrons[SquadronName].Patrol + if Patrol then + return math.random( Patrol.LowInterval, Patrol.HighInterval ) else error( "This squadron does not exist:" .. SquadronName ) end @@ -1184,28 +1114,27 @@ do -- AI_A2G_DISPATCHER -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. -- @return #table DefenderSquadron - function AI_A2G_DISPATCHER:CanCAP( SquadronName ) + function AI_A2G_DISPATCHER:CanPatrol( SquadronName, DefenseTaskType ) self:F({SquadronName = SquadronName}) - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} - local DefenderSquadron = self:GetSquadron( SquadronName ) - if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured. + if DefenderSquadron.Captured == false then -- We can only spawn new Patrol if the base has not been captured. if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. - - local Cap = DefenderSquadron.Cap - if Cap then - local CapCount = self:CountCapAirborne( SquadronName ) - self:F( { CapCount = CapCount } ) - if CapCount < Cap.CapLimit then + + local Patrol = DefenderSquadron[DefenseTaskType] + if Patrol and Patrol.Patrol == true then + local PatrolCount = self:CountPatrolAirborne( SquadronName, DefenseTaskType ) + self:F( { PatrolCount = PatrolCount, PatrolLimit = Patrol.PatrolLimit, PatrolProbability = Patrol.Probability } ) + if PatrolCount < Patrol.PatrolLimit then local Probability = math.random() - if Probability <= Cap.Probability then - return DefenderSquadron + if Probability <= Patrol.Probability then + return DefenderSquadron, Patrol end end + else + self:F( "No patrol for " .. SquadronName ) end end end @@ -1217,7 +1146,7 @@ do -- AI_A2G_DISPATCHER -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. -- @return #table DefenderSquadron - function AI_A2G_DISPATCHER:CanDEFEND( SquadronName, DefenseTaskType ) + function AI_A2G_DISPATCHER:CanDefend( SquadronName, DefenseTaskType ) self:F({SquadronName = SquadronName, DefenseTaskType}) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -1225,7 +1154,7 @@ do -- AI_A2G_DISPATCHER if DefenderSquadron.Captured == false then -- We can only spawn new defense if the home airbase has not been captured. if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. - if DefenderSquadron[DefenseTaskType] then + if DefenderSquadron[DefenseTaskType] and DefenderSquadron[DefenseTaskType].Defend == true then return DefenderSquadron, DefenderSquadron[DefenseTaskType] end end @@ -1257,11 +1186,55 @@ do -- AI_A2G_DISPATCHER Sead.Name = SquadronName Sead.EngageMinSpeed = EngageMinSpeed Sead.EngageMaxSpeed = EngageMaxSpeed + Sead.Defend = true self:F( { Sead = Sead } ) end + --- Set a Sead patrol for a Squadron. + -- The Sead patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Sead Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronSeadPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} + + local SeadPatrol = DefenderSquadron.SEAD + SeadPatrol.Name = SquadronName + SeadPatrol.Zone = Zone + SeadPatrol.FloorAltitude = FloorAltitude + SeadPatrol.CeilingAltitude = CeilingAltitude + SeadPatrol.PatrolMinSpeed = PatrolMinSpeed + SeadPatrol.PatrolMaxSpeed = PatrolMaxSpeed + SeadPatrol.EngageMinSpeed = EngageMinSpeed + SeadPatrol.EngageMaxSpeed = EngageMaxSpeed + SeadPatrol.AltType = AltType + SeadPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "SEAD" ) + + self:F( { Sead = SeadPatrol } ) + end + + --- -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -1285,10 +1258,54 @@ do -- AI_A2G_DISPATCHER Cas.Name = SquadronName Cas.EngageMinSpeed = EngageMinSpeed Cas.EngageMaxSpeed = EngageMaxSpeed + Cas.Defend = true self:F( { Cas = Cas } ) end + + --- Set a Cas patrol for a Squadron. + -- The Cas patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Cas Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronCasPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.CAS = DefenderSquadron.CAS or {} + + local CasPatrol = DefenderSquadron.CAS + CasPatrol.Name = SquadronName + CasPatrol.Zone = Zone + CasPatrol.FloorAltitude = FloorAltitude + CasPatrol.CeilingAltitude = CeilingAltitude + CasPatrol.PatrolMinSpeed = PatrolMinSpeed + CasPatrol.PatrolMaxSpeed = PatrolMaxSpeed + CasPatrol.EngageMinSpeed = EngageMinSpeed + CasPatrol.EngageMaxSpeed = EngageMaxSpeed + CasPatrol.AltType = AltType + CasPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "CAS" ) + + self:F( { Cas = CasPatrol } ) + end + --- -- @param #AI_A2G_DISPATCHER self @@ -1313,10 +1330,54 @@ do -- AI_A2G_DISPATCHER Bai.Name = SquadronName Bai.EngageMinSpeed = EngageMinSpeed Bai.EngageMaxSpeed = EngageMaxSpeed + Bai.Defend = true self:F( { Bai = Bai } ) end + + --- Set a Bai patrol for a Squadron. + -- The Bai patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Bai Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronBaiPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.BAI = DefenderSquadron.BAI or {} + + local BaiPatrol = DefenderSquadron.BAI + BaiPatrol.Name = SquadronName + BaiPatrol.Zone = Zone + BaiPatrol.FloorAltitude = FloorAltitude + BaiPatrol.CeilingAltitude = CeilingAltitude + BaiPatrol.PatrolMinSpeed = PatrolMinSpeed + BaiPatrol.PatrolMaxSpeed = PatrolMaxSpeed + BaiPatrol.EngageMinSpeed = EngageMinSpeed + BaiPatrol.EngageMaxSpeed = EngageMaxSpeed + BaiPatrol.AltType = AltType + BaiPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "BAI" ) + + self:F( { Bai = BaiPatrol } ) + end + --- Defines the default amount of extra planes that will take-off as part of the defense system. -- @param #AI_A2G_DISPATCHER self @@ -1399,7 +1460,7 @@ do -- AI_A2G_DISPATCHER --- Sets the default grouping of new airplanes spawned. -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. -- @param #AI_A2G_DISPATCHER self - -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders. + -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) @@ -1421,7 +1482,7 @@ do -- AI_A2G_DISPATCHER -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The name of the squadron. - -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders. + -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. -- @usage: -- -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) @@ -2064,48 +2125,23 @@ do -- AI_A2G_DISPATCHER end - --- Creates an SWEEP task when there are targets for it. - -- @param #AI_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem - -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. - function AI_A2G_DISPATCHER:EvaluateSWEEP( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local DetectedSet = DetectedItem.Set - local DetectedZone = DetectedItem.Zone - - - if DetectedItem.IsDetected == false then - - -- Here we're doing something advanced... We're copying the DetectedSet. - local TargetSetUnit = SET_UNIT:New() - TargetSetUnit:SetDatabase( DetectedSet ) - TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - - return TargetSetUnit - end - - return nil - end - --- -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:CountCapAirborne( SquadronName ) + function AI_A2G_DISPATCHER:CountPatrolAirborne( SquadronName, DefenseTaskType ) - local CapCount = 0 + local PatrolCount = 0 local DefenderSquadron = self.DefenderSquadrons[SquadronName] if DefenderSquadron then for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do if DefenderTask.SquadronName == SquadronName then - if DefenderTask.Type == "CAP" then + if DefenderTask.Type == DefenseTaskType then if AIGroup:IsAlive() then - -- Check if the CAP is patrolling or engaging. If not, this is not a valid CAP, even if it is alive! - -- The CAP could be damaged, lost control, or out of fuel! + -- Check if the Patrol is patrolling or engaging. If not, this is not a valid Patrol, even if it is alive! + -- The Patrol could be damaged, lost control, or out of fuel! if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) or DefenderTask.Fsm:Is( "Started" ) then - CapCount = CapCount + 1 + PatrolCount = PatrolCount + 1 end end end @@ -2113,7 +2149,7 @@ do -- AI_A2G_DISPATCHER end end - return CapCount + return PatrolCount end @@ -2173,7 +2209,7 @@ do -- AI_A2G_DISPATCHER local DefenderTask = self:GetDefenderTask( FriendlyGroup ) if DefenderTask then -- The Task should be SEAD - if DefenderTask.Type == "SEAD" or DefenderTask.Type == "CAS" or DefenderTask.Type == "BAI" then + if true then -- TODO: fix this to the correct DefenderTaskType -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet if DefenderTask.Target == nil then if DefenderTask.Fsm:Is( "Returning" ) @@ -2196,50 +2232,6 @@ do -- AI_A2G_DISPATCHER end - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:CountDefendersToBeEngaged( AttackerDetection, DefenderCount ) - - local Friendlies = nil - - local AttackerSet = AttackerDetection.Set - local AttackerCount = AttackerSet:Count() - - local DefenderFriendlies = self:GetAIFriendliesNearBy( AttackerDetection ) - - for FriendlyDistance, AIFriendly in UTILS.spairs( DefenderFriendlies or {} ) do - -- We only allow to ENGAGE targets as long as the Units on both sides are balanced. - if AttackerCount > DefenderCount then - local Friendly = AIFriendly:GetGroup() -- Wrapper.Group#GROUP - if Friendly and Friendly:IsAlive() then - -- Ok, so we have a friendly near the potential target. - -- Now we need to check if the AIGroup has a Task. - local DefenderTask = self:GetDefenderTask( Friendly ) - if DefenderTask then - -- The Task should be CAP or GCI - if DefenderTask.Type == "CAP" or DefenderTask.Type == "GCI" then - -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet - if DefenderTask.Target == nil then - if DefenderTask.Fsm:Is( "Returning" ) - or DefenderTask.Fsm:Is( "Patrolling" ) then - Friendlies = Friendlies or {} - Friendlies[Friendly] = Friendly - DefenderCount = DefenderCount + Friendly:GetSize() - self:F( { Friendly = Friendly:GetName(), FriendlyDistance = FriendlyDistance } ) - end - end - end - end - end - else - break - end - end - - return Friendlies - end - - --- -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded ) @@ -2251,7 +2243,7 @@ do -- AI_A2G_DISPATCHER if self:IsSquadronVisible( SquadronName ) then - -- Here we CAP the new planes. + -- Here we Patrol the new planes. -- The Resources table is filled in advance. local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template. @@ -2261,20 +2253,20 @@ do -- AI_A2G_DISPATCHER -- New we will form the group to spawn in. -- We search for the first free resource matching the template. local DefenderUnitIndex = 1 - local DefenderCAPTemplate = nil + local DefenderPatrolTemplate = nil local DefenderName = nil for GroupName, DefenderGroup in pairs( DefenderSquadron.Resources[TemplateID] or {} ) do self:F( { GroupName = GroupName } ) local DefenderTemplate = _DATABASE:GetGroupTemplate( GroupName ) if DefenderUnitIndex == 1 then - DefenderCAPTemplate = UTILS.DeepCopy( DefenderTemplate ) - self.DefenderCAPIndex = self.DefenderCAPIndex + 1 - DefenderCAPTemplate.name = SquadronName .. "#" .. self.DefenderCAPIndex .. "#" .. GroupName - DefenderName = DefenderCAPTemplate.name + DefenderPatrolTemplate = UTILS.DeepCopy( DefenderTemplate ) + self.DefenderPatrolIndex = self.DefenderPatrolIndex + 1 + DefenderPatrolTemplate.name = SquadronName .. "#" .. self.DefenderPatrolIndex .. "#" .. GroupName + DefenderName = DefenderPatrolTemplate.name else - -- Add the unit in the template to the DefenderCAPTemplate. + -- Add the unit in the template to the DefenderPatrolTemplate. local DefenderUnitTemplate = DefenderTemplate.units[1] - DefenderCAPTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate + DefenderPatrolTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate end DefenderUnitIndex = DefenderUnitIndex + 1 DefenderSquadron.Resources[TemplateID][GroupName] = nil @@ -2284,15 +2276,15 @@ do -- AI_A2G_DISPATCHER end - if DefenderCAPTemplate then + if DefenderPatrolTemplate then local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) local SpawnGroup = GROUP:Register( DefenderName ) - DefenderCAPTemplate.lateActivation = nil - DefenderCAPTemplate.uncontrolled = nil + DefenderPatrolTemplate.lateActivation = nil + DefenderPatrolTemplate.uncontrolled = nil local Takeoff = self:GetSquadronTakeoff( SquadronName ) - DefenderCAPTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type - DefenderCAPTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action - local Defender = _DATABASE:Spawn( DefenderCAPTemplate ) + DefenderPatrolTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + DefenderPatrolTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + local Defender = _DATABASE:Spawn( DefenderPatrolTemplate ) self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) return Defender, DefenderGrouping @@ -2316,72 +2308,65 @@ do -- AI_A2G_DISPATCHER --- -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:onafterCAP( From, Event, To, SquadronName ) + function AI_A2G_DISPATCHER:onafterPatrol( From, Event, To, SquadronName, DefenseTaskType ) self:F({SquadronName = SquadronName}) - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} - local DefenderSquadron = self:CanCAP( SquadronName ) + local DefenderSquadron, Patrol = self:CanPatrol( SquadronName, DefenseTaskType ) - if DefenderSquadron then - - local Cap = DefenderSquadron.Cap - - if Cap then + if Patrol then - local DefenderCAP, DefenderGrouping = self:ResourceActivate( DefenderSquadron ) - - if DefenderCAP then - - local Fsm = AI_A2G_CAP:New( DefenderCAP, Cap.Zone, Cap.FloorAltitude, Cap.CeilingAltitude, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.AltType ) - Fsm:SetDispatcher( self ) - Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) - Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) - Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) - Fsm:SetDisengageRadius( self.DisengageRadius ) - Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) - Fsm:Start() - - self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", Fsm ) + local DefenderPatrol, DefenderGrouping = self:ResourceActivate( DefenderSquadron ) + + if DefenderPatrol then - function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"CAP Birth", Defender:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local Fsm = AI_A2G_PATROL:New( DefenderPatrol, Patrol.Zone, Patrol.FloorAltitude, Patrol.CeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.AltType ) + Fsm:SetDispatcher( self ) + Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + Fsm:SetDisengageRadius( self.DisengageRadius ) + Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) + Fsm:Start() - if Squadron then - Fsm:__Patrol( 2 ) -- Start Patrolling - end + self:SetDefenderTask( SquadronName, DefenderPatrol, DefenseTaskType, Fsm ) + + function Fsm:onafterTakeoff( Defender, From, Event, To ) + self:F({"Patrol Birth", Defender:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + + if Squadron then + Fsm:__Patrol( 2 ) -- Start Patrolling end - - function Fsm:onafterRTB( Defender, From, Event, To ) - self:F({"CAP RTB", Defender:GetName()}) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) - local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - Dispatcher:ClearDefenderTaskTarget( Defender ) + end + + function Fsm:onafterRTB( Defender, From, Event, To ) + self:F({"Patrol RTB", Defender:GetName()}) + self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + Dispatcher:ClearDefenderTaskTarget( Defender ) + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterHome( Defender, From, Event, To, Action ) + self:F({"Patrol Home", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() end - - --- @param #AI_A2G_DISPATCHER self - function Fsm:onafterHome( Defender, From, Event, To, Action ) - self:F({"CAP Home", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - - local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - - if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - - if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - self:ParkDefender( Squadron, Defender ) - end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + self:ParkDefender( Squadron, Defender ) end end end @@ -2392,7 +2377,7 @@ do -- AI_A2G_DISPATCHER --- -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:onafterENGAGE( From, Event, To, AttackerDetection, Defenders ) + function AI_A2G_DISPATCHER:onafterEngage( From, Event, To, AttackerDetection, Defenders ) if Defenders then @@ -2409,7 +2394,7 @@ do -- AI_A2G_DISPATCHER --- -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:onafterDEFEND( From, Event, To, AttackerDetection, DefendersMissing, DefenderFriendlies, DefenseTaskType ) + function AI_A2G_DISPATCHER:onafterDefend( From, Event, To, AttackerDetection, DefendersMissing, DefenderFriendlies, DefenseTaskType ) self:F( { From, Event, To, AttackerDetection.Index, DefendersMissing, DefenderFriendlies } ) @@ -2469,7 +2454,7 @@ do -- AI_A2G_DISPATCHER if ClosestDefenderSquadronName then - local DefenderSquadron, Defense = self:CanDEFEND( ClosestDefenderSquadronName, DefenseTaskType ) + local DefenderSquadron, Defense = self:CanDefend( ClosestDefenderSquadronName, DefenseTaskType ) if Defense then @@ -2757,7 +2742,7 @@ do -- AI_A2G_DISPATCHER local DistanceProbability = ( self.DefenseDistance / EvaluateDistance * self.DefenseReactivity ) local DefenseProbability = math.random() - self:F({DistanceProbability=DistanceProbability,DefenseProbability=DefenseProbability}) + self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then DefenseCoordinate = EvaluateCoordinate @@ -2770,7 +2755,7 @@ do -- AI_A2G_DISPATCHER local DefendersMissing, Friendlies = self:Evaluate_SEAD( DetectedItem ) -- Returns a SET_UNIT with the SEAD targets to be engaged... if DefendersMissing and DefendersMissing > 0 then self:F( { SeadGroups = Friendlies } ) - self:__DEFEND( Delay, DetectedItem, DefendersMissing, Friendlies, "SEAD", DefenseCoordinate ) + self:__Defend( Delay, DetectedItem, DefendersMissing, Friendlies, "SEAD", DefenseCoordinate ) Delay = Delay + 1 end end @@ -2779,7 +2764,7 @@ do -- AI_A2G_DISPATCHER local DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... if DefendersMissing and DefendersMissing > 0 then self:F( { CasGroups = Friendlies } ) - self:__DEFEND( Delay, DetectedItem, DefendersMissing, Friendlies, "CAS", DefenseCoordinate ) + self:__Defend( Delay, DetectedItem, DefendersMissing, Friendlies, "CAS", DefenseCoordinate ) Delay = Delay + 1 end end @@ -2788,7 +2773,7 @@ do -- AI_A2G_DISPATCHER local DefendersMissing, Friendlies = self:Evaluate_BAI( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... if DefendersMissing and DefendersMissing > 0 then self:F( { BaiGroups = Friendlies } ) - self:__DEFEND( Delay, DetectedItem, DefendersMissing, Friendlies, "BAI", DefenseCoordinate ) + self:__Defend( Delay, DetectedItem, DefendersMissing, Friendlies, "BAI", DefenseCoordinate ) Delay = Delay + 1 end end @@ -2952,11 +2937,13 @@ do return FriendliesCount, FriendlyTypesReport end - --- Schedules a new CAP for the given SquadronName. + --- Schedules a new Patrol for the given SquadronName. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - function AI_A2G_DISPATCHER:SchedulerCAP( SquadronName ) - self:CAP( SquadronName ) + function AI_A2G_DISPATCHER:SchedulerPatrol( SquadronName ) + local PatrolTaskTypes = { "SEAD", "CAS", "BAI" } + local PatrolTaskType = PatrolTaskTypes[math.random(1,3)] + self:Patrol( SquadronName, PatrolTaskType ) end end @@ -3122,7 +3109,7 @@ do -- -- You can change or add a CAP zone by using the inherited methods from AI\_A2G\_DISPATCHER: -- - -- The method @{#AI_A2G_DISPATCHER.SetSquadronCap}() defines a CAP execution for a squadron. + -- The method @{#AI_A2G_DISPATCHER.SetSquadronPatrol}() defines a CAP execution for a squadron. -- -- Setting-up a CAP zone also requires specific parameters: -- @@ -3133,14 +3120,14 @@ do -- -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. -- - -- The @{#AI_A2G_DISPATCHER.SetSquadronCapInterval}() method specifies **how much** and **when** CAP flights will takeoff. + -- The @{#AI_A2G_DISPATCHER.SetSquadronPatrolInterval}() method specifies **how much** and **when** CAP flights will takeoff. -- -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system. -- -- For example, the following setup will create a CAP for squadron "Sochi": -- - -- A2GDispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) - -- A2GDispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- A2GDispatcher:SetSquadronPatrol( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Sochi", 2, 30, 120, 1 ) -- -- ### 2.4) Each airbase will perform GCI when required, with the following parameters: -- @@ -3227,8 +3214,8 @@ do -- @param #AI_A2G_GCICAP self -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. -- @param #string TemplatePrefixes A list of template prefixes. - -- @param #string CapPrefixes A list of CAP zone prefixes (polygon zones). - -- @param #number CapLimit A number of how many CAP maximum will be spawned. + -- @param #string PatrolPrefixes A list of CAP zone prefixes (polygon zones). + -- @param #number PatrolLimit A number of how many CAP maximum will be spawned. -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. @@ -3308,7 +3295,7 @@ do -- -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, nil, nil, nil, nil, nil, 30 ) -- - function AI_A2G_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + function AI_A2G_GCICAP:New( EWRPrefixes, TemplatePrefixes, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) local EWRSetGroup = SET_GROUP:New() EWRSetGroup:FilterPrefixes( EWRPrefixes ) @@ -3370,7 +3357,7 @@ do -- CAP will be launched from there. self.CAPTemplates = SET_GROUP:New() - self.CAPTemplates:FilterPrefixes( CapPrefixes ) + self.CAPTemplates:FilterPrefixes( PatrolPrefixes ) self.CAPTemplates:FilterOnce() self:I( "Setting up CAP ..." ) @@ -3396,8 +3383,8 @@ do end if AirbaseClosest then self:I( { CAPAirbase = AirbaseClosest:GetName() } ) - self:SetSquadronCap( AirbaseClosest:GetName(), CAPZone, 6000, 10000, 500, 800, 800, 1200, "RADIO" ) - self:SetSquadronCapInterval( AirbaseClosest:GetName(), CapLimit, 300, 600, 1 ) + self:SetSquadronPatrol( AirbaseClosest:GetName(), CAPZone, 6000, 10000, 500, 800, 800, 1200, "RADIO" ) + self:SetSquadronPatrolInterval( AirbaseClosest:GetName(), PatrolLimit, 300, 600, 1 ) end end @@ -3432,8 +3419,8 @@ do -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. -- @param #string TemplatePrefixes A list of template prefixes. -- @param #string BorderPrefix A Border Zone Prefix. - -- @param #string CapPrefixes A list of CAP zone prefixes (polygon zones). - -- @param #number CapLimit A number of how many CAP maximum will be spawned. + -- @param #string PatrolPrefixes A list of CAP zone prefixes (polygon zones). + -- @param #number PatrolLimit A number of how many CAP maximum will be spawned. -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. @@ -3522,9 +3509,9 @@ do -- -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", nil, nil, nil, nil, nil, 30 ) -- - function AI_A2G_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + function AI_A2G_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) - local self = AI_A2G_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + local self = AI_A2G_GCICAP:New( EWRPrefixes, TemplatePrefixes, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) if BorderPrefix then self:SetBorderZone( ZONE_POLYGON:New( BorderPrefix, GROUP:FindByName( BorderPrefix ) ) ) diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua new file mode 100644 index 000000000..dcf4b2acf --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_Patrol.lua @@ -0,0 +1,488 @@ +--- **AI** -- Models the process of A2G patrolling and engaging ground targets for airplanes and helicopters. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_Patrol +-- @image AI_Combat_Air_Patrol.JPG + +--- @type AI_A2G_PATROL +-- @extends AI.AI_A2A_Patrol#AI_A2A_PATROL + + +--- The AI_A2G_PATROL class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} +-- and automatically engage any airborne enemies that are within a certain range or within a certain zone. +-- +-- ![Process](..\Presentations\AI_CAP\Dia3.JPG) +-- +-- The AI_A2G_PATROL is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_PATROL process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_CAP\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_CAP\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_CAP\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_CAP\Dia13.JPG) +-- +-- ## 1. AI_A2G_PATROL constructor +-- +-- * @{#AI_A2G_PATROL.New}(): Creates a new AI_A2G_PATROL object. +-- +-- ## 2. AI_A2G_PATROL is a FSM +-- +-- ![Process](..\Presentations\AI_CAP\Dia2.JPG) +-- +-- ### 2.1 AI_A2G_PATROL States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the bogeys. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 2.2 AI_A2G_PATROL Events +-- +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{#AI_A2G_PATROL.Engage}**: Let the AI engage the bogeys. +-- * **@{#AI_A2G_PATROL.Abort}**: Aborts the engagement and return patrolling in the patrol zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_A2G_PATROL.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. +-- * **@{#AI_A2G_PATROL.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_CAP\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_CAP#AI_A2G_PATROL.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_A2G_PATROL.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2G_PATROL +AI_A2G_PATROL = { + ClassName = "AI_A2G_PATROL", +} + +--- Creates a new AI_A2G_PATROL object +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_PATROL +function AI_A2G_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2A_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_PATROL + + self.Accomplished = false + self.Engaging = false + + self.EngageMinSpeed = EngageMinSpeed + self.EngageMaxSpeed = EngageMaxSpeed + + self:AddTransition( { "Patrolling", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_A2G_PATROL] OnBeforeEngage + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_A2G_PATROL] OnAfterEngage + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_A2G_PATROL] Engage + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_A2G_PATROL] __Engage + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_A2G_PATROL] OnLeaveEngaging +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_A2G_PATROL] OnEnterEngaging +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_A2G_PATROL] OnBeforeFired + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_A2G_PATROL] OnAfterFired + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_A2G_PATROL] Fired + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_A2G_PATROL] __Fired + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_A2G_PATROL] OnBeforeDestroy + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_A2G_PATROL] OnAfterDestroy + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_A2G_PATROL] Destroy + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_A2G_PATROL] __Destroy + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_A2G_PATROL] OnBeforeAbort + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_A2G_PATROL] OnAfterAbort + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_A2G_PATROL] Abort + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_A2G_PATROL] __Abort + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_A2G_PATROL] OnBeforeAccomplish + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_A2G_PATROL] OnAfterAccomplish + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_A2G_PATROL] Accomplish + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_A2G_PATROL] __Accomplish + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + + return self +end + + +--- onafter State Transition for Event Patrol. +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onafterStart( AIPatrol, From, Event, To ) + + self:GetParent( self ).onafterStart( self, AIPatrol, From, Event, To ) + AIPatrol:HandleEvent( EVENTS.Takeoff, nil, self ) + +end + +--- Set the Engage Zone which defines where the AI will engage bogies. +-- @param #AI_A2G_PATROL self +-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. +-- @return #AI_A2G_PATROL self +function AI_A2G_PATROL:SetEngageZone( EngageZone ) + self:F2() + + if EngageZone then + self.EngageZone = EngageZone + else + self.EngageZone = nil + end +end + +--- Set the Engage Range when the AI will engage with airborne enemies. +-- @param #AI_A2G_PATROL self +-- @param #number EngageRange The Engage Range. +-- @return #AI_A2G_PATROL self +function AI_A2G_PATROL:SetEngageRange( EngageRange ) + self:F2() + + if EngageRange then + self.EngageRange = EngageRange + else + self.EngageRange = nil + end +end + +--- onafter State Transition for Event Patrol. +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onafterPatrol( AIPatrol, From, Event, To ) + + -- Call the parent Start event handler + self:GetParent(self).onafterPatrol( self, AIPatrol, From, Event, To ) + self:HandleEvent( EVENTS.Dead ) + +end + +-- todo: need to fix this global function + +--- @param Wrapper.Group#GROUP AIPatrol +function AI_A2G_PATROL.AttackRoute( AIPatrol, Fsm ) + + AIPatrol:F( { "AI_A2G_PATROL.AttackRoute:", AIPatrol:GetName() } ) + + if AIPatrol:IsAlive() then + Fsm:__Engage( 0.5 ) + end +end + +--- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onbeforeEngage( AIPatrol, From, Event, To ) + + if self.Accomplished == true then + return false + end +end + +--- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onafterAbort( AIPatrol, From, Event, To ) + AIPatrol:ClearTasks() + self:__Route( 0.5 ) +end + + +--- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The AIPatrol Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onafterEngage( AIPatrol, From, Event, To, AttackSetUnit ) + + self:F( { AIPatrol, From, Event, To, AttackSetUnit} ) + + self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + + local FirstAttackUnit = self.AttackSetUnit:GetFirst() -- Wrapper.Unit#UNIT + + if FirstAttackUnit and FirstAttackUnit:IsAlive() then -- If there is no attacker anymore, stop the engagement. + + if AIPatrol:IsAlive() then + + local EngageRoute = {} + + --- Calculate the target route point. + local CurrentCoord = AIPatrol:GetCoordinate() + local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + local ToInterceptAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) + + --- Create a route point of type air. + local ToPatrolRoutePoint = CurrentCoord:Translate( 5000, ToInterceptAngle ):WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = ToInterceptAngle, ToTargetSpeed = ToTargetSpeed } ) + self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint + EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint + + local AttackTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT + self:T( { "Attacking Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) + if AttackUnit:IsAlive() and AttackUnit:IsAir() then + AttackTasks[#AttackTasks+1] = AIPatrol:TaskAttackUnit( AttackUnit ) + end + end + + if #AttackTasks == 0 then + self:E("No targets found -> Going back to Patrolling") + self:__Abort( 0.5 ) + else + AIPatrol:OptionROEOpenFire() + AIPatrol:OptionROTEvadeFire() + + AttackTasks[#AttackTasks+1] = AIPatrol:TaskFunction( "AI_A2G_PATROL.AttackRoute", self ) + EngageRoute[#EngageRoute].task = AIPatrol:TaskCombo( AttackTasks ) + end + + AIPatrol:Route( EngageRoute, 0.5 ) + end + else + self:E("No targets found -> Going back to Patrolling") + self:__Abort( 0.5 ) + end +end + +--- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onafterAccomplish( AIPatrol, From, Event, To ) + self.Accomplished = true + self:SetDetectionOff() +end + +--- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_A2G_PATROL:onafterDestroy( AIPatrol, From, Event, To, EventData ) + + if EventData.IniUnit then + self.AttackUnits[EventData.IniUnit] = nil + end +end + +--- @param #AI_A2G_PATROL self +-- @param Core.Event#EVENTDATA EventData +function AI_A2G_PATROL:OnEventDead( EventData ) + self:F( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then + self:__Destroy( 1, EventData ) + end + end +end + +--- @param Wrapper.Group#GROUP AIPatrol +function AI_A2G_PATROL.Resume( AIPatrol, Fsm ) + + AIPatrol:I( { "AI_A2G_PATROL.Resume:", AIPatrol:GetName() } ) + if AIPatrol:IsAlive() then + Fsm:__Reset( 1 ) + Fsm:__Route( 5 ) + end + +end diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 4ddc474e0..231861fe0 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -68,6 +68,7 @@ AI/AI_A2A_Gci.lua AI/AI_A2A_Dispatcher.lua AI/AI_A2G.lua AI/AI_A2G_Engage.lua +AI/AI_A2G_Patrol.lua AI/AI_A2G_Dispatcher.lua AI/AI_Patrol.lua AI/AI_Cap.lua From b21bb3d433e8161f85d6c9c5bff0d9bde1bb36fb Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 7 Nov 2018 16:08:13 +0100 Subject: [PATCH 018/147] CT v0.1.9w --- .../Moose/Functional/CarrierTrainer.lua | 90 +++++++++++++++---- 1 file changed, 73 insertions(+), 17 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 810f8eb87..64342e12a 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -230,7 +230,7 @@ CARRIERTRAINER.GroovePos={ -- @field #number passes Number of passes. -- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. -- @field #table debrief Debrief analysis of the current step of this pass. --- @field #table grade LSO grade of passes. +-- @field #table grades LSO grades of player passes. -- @field #boolean inbigzone If true, player is in the big zone. -- @field #boolean landed If true, player landed or attempted to land. -- @field #boolean bolter If true, LSO told player to bolter. @@ -264,7 +264,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.9" +CARRIERTRAINER.version="0.1.9w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1483,6 +1483,14 @@ function CARRIERTRAINER:_Debrief(playerData) -- LSO grade, points, and flight data analyis. local grade, points, analysis=self:_LSOgrade(playerData) + + local mygrade={} --#CARRIERTRAINER.LSOgrade + mygrade.grade=grade + mygrade.points=points + mygrade.details=analysis + + table.insert(playerData.grades, mygrade) + -- LSO grade message. text=string.format("%s %.1f PT - %s", grade, points, analysis) self:_SendMessageToPlayer(text, 30, playerData, true, "Paddles", 30) @@ -2256,17 +2264,16 @@ function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData, clea delay=delay or 0 sender=sender or self.alias - - if playerData.client then - --MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToClient(playerData.client) - end - + local text=string.format("%s, %s, %s", sender, playerData.callsign, message) env.info(text) if delay>0 then SCHEDULER:New(nil,self._SendMessageToPlayer, {self, message, duration, playerData, clear, sender}, delay) - else + else + if playerData.client then + --MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToClient(playerData.client) + end MESSAGE:New(text, duration, nil, clear):ToAll() end @@ -2343,15 +2350,15 @@ function CARRIERTRAINER:_AddF10Commands(_unitName) local _trainPath = missionCommands.addSubMenuForGroup(_gid, self.alias, CARRIERTRAINER.MenuF10[_gid]) -- F10/Carrier Trainer//Results - local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Results", _trainPath) + local _statsPath = missionCommands.addSubMenuForGroup(_gid, "LSO Grades", _trainPath) -- F10/Carrier Trainer//My Settings/Difficulty local _difficulPath = missionCommands.addSubMenuForGroup(_gid, "Difficulty", _trainPath) -- F10/Carrier Trainer//Results/ -- TODO: Add result functions. - --missionCommands.addCommandForGroup(_gid, "All Results", _statsPath, self._DisplayStrafePitResults, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "My Results", _statsPath, self._DisplayBombingResults, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Greenie Board", _statsPath, self._DisplayScoreBoard, self, _unitName) + missionCommands.addCommandForGroup(_gid, "My Grades", _statsPath, self._DisplayPlayerGrades, self, _unitName) --missionCommands.addCommandForGroup(_gid, "(Clear ALL Results)", _statsPath, self._ResetRangeStats, self, _unitName) -- F10/Carrier Trainer//Difficulty @@ -2375,6 +2382,48 @@ function CARRIERTRAINER:_AddF10Commands(_unitName) end +--- Display top 10 player scores. +-- @param #CARRIERTRAINER self +-- @param #string _unitName Name fo the player unit. +function CARRIERTRAINER:_DisplayPlayerGrades(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#CARRIERTRAINTER.PlayerData + + if playerData then + local text=string.format("Your grades, %s:", _playername) + local p=0 + for i,_grade in pairs(playerData.grades) do + local grade=_grade --#CARRIERTRAINER.LSOgrade + + text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, grade.points, grade.details) + p=p+grade.points + end + + -- Number of grades. + local n=#playerData.grades + + if n>0 then + text=text..string.format("\nAverage points = %.1f", p/n) + else + text=text..string.format("No data available.") + end + + -- Send message. + if playerData.client then + --MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToClient(playerData.client) + end + MESSAGE:New(text, 30, nil, true):ToAll() + + end + end +end + --- Display top 10 player scores. -- @param #CARRIERTRAINER self @@ -2395,12 +2444,14 @@ function CARRIERTRAINER:_DisplayScoreBoard(_unitName) local _message = string.format("Greenie Board:\n") -- Loop over player results. - for _playerName,_results in pairs(self.strafePlayerResults) do + for _playerName,_grades in pairs(self.playerGrades) do + -- Get the best result of the player. local _best=nil - for _,_result in pairs(_results) do - if _best==nil or _result.hits > _best.hits then + for _,_grade in pairs(_grades) do + local grade=_grade --#CARRIERTRAINTER.LSOgrade + if _best==nil or grade.po > _best.hits then _best = _result end end @@ -2414,11 +2465,16 @@ function CARRIERTRAINER:_DisplayScoreBoard(_unitName) end --Sort list! - local _sort=function(a, b) return a.hits>b.hits end - table.sort(_playerResults,_sort) + local _sort=function(a, b) return a.points>b.points end + table.sort(self.playerGrades,_sort) + + local _i=0 + for _playername,_grade in pairs(self.playerGrades) do + _message=_message..string.format("\n[%d] %s", _i, _grade) + end -- Add top 10 results. - for _i = 1, math.min(#_playerResults, self.ndisplayresult) do + for _i = 1, 10 do --math.min(#_playerResults, self.ndisplayresult) do _message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg) end From ac074ca23d0679b71c29db0643ec67bdf4fdd46c Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 7 Nov 2018 23:26:51 +0100 Subject: [PATCH 019/147] CT v0.2.0 --- .../Moose/Functional/CarrierTrainer.lua | 271 +++++++++--------- 1 file changed, 130 insertions(+), 141 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 64342e12a..139b736be 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -264,7 +264,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.1.9w" +CARRIERTRAINER.version="0.2.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -280,6 +280,7 @@ CARRIERTRAINER.version="0.1.9w" -- TODO: Generalize parameters for other aircraft. -- TODO: CASE II. -- TODO: CASE III. +-- TODO: Foul deck check. -- DONE: Fix radio menu. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -566,7 +567,7 @@ function CARRIERTRAINER:OnEventBirth(EventData) self.players[_playername]=self:_InitPlayer(_unitName) -- Start in the groove for debugging. - self.groovedebug=true + self.groovedebug=false end end @@ -612,8 +613,11 @@ function CARRIERTRAINER:OnEventLand(EventData) local w4=self.carrier:GetCoordinate():Translate( -68, 0):MarkToAll("Wire 4") -- We did land. + env.info("FF landed") playerData.landed=true + playerData.step=-1 + --TODO: maybe check that we actually landed on the right carrier. -- Call trapped function in 3 seconds to make sure we did not bolter. @@ -669,7 +673,7 @@ function CARRIERTRAINER:_InitNewRound(playerData) self:I(self.lid..string.format("New round for player %s.", playerData.callsign)) playerData.step=0 playerData.score=100 - playerData.groove={} + playerData.groove={} playerData.debrief={} playerData.patternwo=false playerData.lig=false @@ -838,12 +842,11 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) playerData.lig=true -- Next step: Debriefing. - playerData.step=999 + playerData.step=999 end end - --- Abeam. -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data table. @@ -1128,33 +1131,34 @@ function CARRIERTRAINER:_CallTheBall(playerData) elseif rho<=RIC and playerData.step==94 then - --TODO: grade for IC, call wave off? - self:_SendMessageToPlayer("IC", 8, playerData) - env.info(string.format("FF IC=%d", rho)) - - -- Store data. - playerData.groove.IC=groovedata - - -- Check if player should wave off. - local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA) - - -- Let's see.. - if waveoff then - - -- Wave off player. - self:_SendMessageToPlayer(CARRIERTRAINER.LSOcall.WAVEOFFT, 10, playerData) - CARRIERTRAINER.LSOcall.WAVEOFF:ToGroup(playerData.unit:GetGroup()) - playerData.Tlso=timer.getTime() + if playerData.waveoff==false then + + self:_SendMessageToPlayer("IC", 8, playerData) + env.info(string.format("FF IC=%d", rho)) - playerData.waveoff=true + -- Store data. + playerData.groove.IC=groovedata - -- Next step: debrief. - playerData.step=999 + -- Check if player should wave off. + local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA) + + -- Let's see.. + if waveoff then + + -- Wave off player. + self:_SendMessageToPlayer(CARRIERTRAINER.LSOcall.WAVEOFFT, 10, playerData) + CARRIERTRAINER.LSOcall.WAVEOFF:ToGroup(playerData.unit:GetGroup()) + playerData.Tlso=timer.getTime() + + -- Player was waved off! + playerData.waveoff=true + + return + else + -- Next step: at the ramp. + playerData.step=95 + end - return - else - -- Next step: at the ramp. - playerData.step=95 end elseif rho<=RAR and playerData.step==95 then @@ -1180,32 +1184,25 @@ function CARRIERTRAINER:_CallTheBall(playerData) -- LSO call if necessary. self:_LSOcall(playerData, glideslopeError, lineupError) - elseif X>0 then + elseif X>100 then if playerData.landed then - - local hint="You boltered." - - -- Send message to player. - self:_SendMessageToPlayer(hint, 8, playerData) -- Add to debrief. - self:_AddToSummary(playerData, "Bolter", hint) + if playerData.waveoff then + self:_AddToSummary(playerData, "Wave Off", "You were waved off but landed anyway. Airboss wants to talk to you!") + else + self:_AddToSummary(playerData, "Bolter", "You boltered.") + end else - - local hint="You were waved off." - - -- Send message to player. - self:_SendMessageToPlayer(hint, 8, playerData) -- Add to debrief. - self:_AddToSummary(playerData, "Wave Off", hint) + self:_AddToSummary(playerData, "Wave Off", "You were waved off.") + -- Next step: debrief. + playerData.step=999 end - - -- Next step: debrief. - playerData.step=999 end end @@ -1225,17 +1222,20 @@ function CARRIERTRAINER:_CheckWaveOff(glideslopeError, lineupError, AoA) local waveoff=false -- Too high or too low? - if math.abs(glideslopeError)>3 then + if math.abs(glideslopeError)>1 then + self:I(self.lid.."Wave off due to glide slope error >1 degrees!") waveoff=true end -- Too far from centerline? if math.abs(lineupError)>3 then + self:I(self.lid.."Wave off due to line up error >3 degrees!") waveoff=true end -- Too slow or too fast? if AoA<6.9 or AoA>9.3 then + self:I(self.lid.."Wave off due to AoA<6.9 or AoA>9.3!") waveoff=true end @@ -1272,19 +1272,19 @@ end -- @param Core.Point#COORDINATE pos Position of aircraft on landing event. function CARRIERTRAINER:_Trapped(playerData, pos) + env.info("FF TRAPPED") + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi = self:_GetDistances(pos) if playerData.unit:InAir()==false then -- Seems we have successfully landed. - local wire = 1 - local score = -10 - -- Little offset for the exact wire positions. local wdx=11 -- Which wire was caught? + local wire if X<-104+wdx then wire=1 elseif X<-92+wdx then @@ -1298,14 +1298,14 @@ function CARRIERTRAINER:_Trapped(playerData, pos) end local text=string.format("TRAPPED! %d-wire.", wire) - self:_SendMessageToPlayer(text, 30, playerData) + self:_SendMessageToPlayer(text, 10, playerData) local text2=string.format("Distance X=%.1f meters resulted in a %d-wire estimate.", X, 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) + local hint = string.format("Trapped catching the %d-wire.", wire) + self:_AddToSummary(playerData, "Recovered", hint) else --Boltered! @@ -1323,12 +1323,11 @@ end -- @param #number lineupError Error in degrees. function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) - local text="" - -- Player group. local player=playerData.unit:GetGroup() -- Glideslope high/low calls. + local text="" if glideslopeError>1 then text="You're high!" CARRIERTRAINER.LSOcall.HIGHL:ToGroup(player) @@ -1431,13 +1430,7 @@ function CARRIERTRAINER:_Lineup(playerData) -- Position of the aircraft wrt carrier coordinates. local a={x=X, z=Z} - - --a.x=-200 - --a.y= 0 - --a.z=17.632698070846 --(100)*math.tan(math.rad(10)) - --a.z=20 - --print(a.z) - + -- Vector from plane to ref point on boad. local c={x=b.x-a.x, y=0, z=b.z-a.z} @@ -1465,6 +1458,7 @@ end -- @param #CARRIERTRAINER self -- @param #CARRIERTRAINER.PlayerData playerData Player data. function CARRIERTRAINER:_Debrief(playerData) + env.info("FF debrief") -- Debriefing text. local text=string.format("Debriefing:\n") @@ -1474,7 +1468,6 @@ function CARRIERTRAINER:_Debrief(playerData) local comment=_data.hint text=text..string.format("* %s:\n",step) text=text..string.format("%s\n", comment) - --text=text..string.format("------------------------------------------------------------\n") end -- Send debrief message to player @@ -1482,27 +1475,25 @@ function CARRIERTRAINER:_Debrief(playerData) -- LSO grade, points, and flight data analyis. local grade, points, analysis=self:_LSOgrade(playerData) - - + local mygrade={} --#CARRIERTRAINER.LSOgrade mygrade.grade=grade mygrade.points=points mygrade.details=analysis + -- Add to table. table.insert(playerData.grades, mygrade) -- LSO grade message. text=string.format("%s %.1f PT - %s", grade, points, analysis) - self:_SendMessageToPlayer(text, 30, playerData, true, "Paddles", 30) - - --TODO: Add grade to table. + self:_SendMessageToPlayer(text, 10, playerData, true, "Paddles", 30) -- New approach. if playerData.boltered or playerData.waveoff or playerData.patternwo then local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) local text=string.format("fly heading %d for %d NM to restart the pattern.", heading, UTILS.MetersToNM(distance)) - self:_SendMessageToPlayer(text, 10, playerData, false, nil, 20) + self:_SendMessageToPlayer(text, 10, playerData, false, nil, 30) end -- Next step. @@ -1959,17 +1950,19 @@ function CARRIERTRAINER:_LSOgrade(playerData) end end - env.info("LSO grade:") - env.info(G) + -- Replace" )"( and "__" G=G:gsub("%)%(", "") - G=G:gsub("__","") - env.info(G) - env.info("Grade = "..grade.." points = "..points) - env.info("# of total deviations = "..N) - env.info("# of large deviations _ = "..nL) - env.info("# of norma deviations _ = "..nN) - env.info("# of small deviations ( = "..nS) - env.info() + G=G:gsub("__","") + + -- Debug info + local text="LSO grade:\n" + text=text..G.."\n" + text=text.."Grade = "..grade.." points = "..points.."\n" + text=text.."# of total deviations = "..N.."\n" + text=text.."# of large deviations _ = "..nL.."\n" + text=text.."# of norma deviations _ = "..nN.."\n" + text=text.."# of small deviations ( = "..nS.."\n" + self:I(self.lid..text) if playerData.patternwo or playerData.waveoff then grade="CUT" @@ -1978,7 +1971,7 @@ function CARRIERTRAINER:_LSOgrade(playerData) G="LIG PWO" elseif playerData.patternwo then G="PWO "..G - end + end if playerData.landed then --AIRBOSS wants to talk to you! end @@ -2006,7 +1999,7 @@ function CARRIERTRAINER:_Flightdata2Text(fdata) -- No flight data ==> return empty string. if fdata==nil then - env.info("FF fdata nil") + self:E(self.lid.."Flight data is nil.") return "", 0 end @@ -2082,15 +2075,20 @@ function CARRIERTRAINER:_Flightdata2Text(fdata) end -- Add current step. + local step=self:_GS(step) + step=step:gsub("XX","X") if G~="" then - G=G..self:_GS(step) + G=G..step end - env.info(string.format("AOA=%.1f",AOA)) - env.info(string.format("GSE=%.1f",GSE)) - env.info(string.format("LUE=%.1f",LUE)) - env.info(string.format("ROL=%.1f",ROL)) - env.info(G) + -- Debug info. + local text=string.format("LSO Grade at %s:\n", step) + text=text..string.format("AOA=%.1f\n",AOA) + text=text..string.format("GSE=%.1f\n",GSE) + text=text..string.format("LUE=%.1f\n",LUE) + text=text..string.format("ROL=%.1f\n",ROL) + text=text..G + self:T(self.lid..text) return G,n end @@ -2151,9 +2149,9 @@ function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint, altitude) -- Extend or decrease depending on skill. if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then - hint=hint..string.format("Optimal altitude is %d ft.\n", UTILS.MetersToFeet(checkpoint.Altitude)) + hint=hint..string.format("Optimal altitude is %d ft.", UTILS.MetersToFeet(checkpoint.Altitude)) elseif playerData.difficulty==CARRIERTRAINER.Difficulty.NORMAL then - hint=hint.."\n" + --hint=hint.."\n" elseif playerData.difficulty==CARRIERTRAINER.Difficulty.HARD then hint="" end @@ -2196,7 +2194,7 @@ function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance) if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then hint=hint..string.format(" Optimal distance is %d NM.", UTILS.MetersToNM(checkpoint.Distance)) elseif playerData.difficulty==CARRIERTRAINER.Difficulty.NORMAL then - hint=hint.."\n" + --hint=hint.."\n" elseif playerData.difficulty==CARRIERTRAINER.Difficulty.HARD then hint="" end @@ -2239,7 +2237,7 @@ function CARRIERTRAINER:_AoACheck(playerData, checkpoint, aoa) if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then hint=hint..string.format(" Optimal AoA is %.1f.", checkpoint.AoA) elseif playerData.difficulty==CARRIERTRAINER.Difficulty.NORMAL then - hint=hint.."\n" + --hint=hint.."\n" elseif playerData.difficulty==CARRIERTRAINER.Difficulty.HARD then hint="" end @@ -2272,9 +2270,9 @@ function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData, clea SCHEDULER:New(nil,self._SendMessageToPlayer, {self, message, duration, playerData, clear, sender}, delay) else if playerData.client then - --MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToClient(playerData.client) + MESSAGE:New(text, duration, nil, clear):ToClient(playerData.client) end - MESSAGE:New(text, duration, nil, clear):ToAll() + --MESSAGE:New(text, duration, nil, clear):ToAll() end end @@ -2347,16 +2345,15 @@ function CARRIERTRAINER:_AddF10Commands(_unitName) local playerData=self.players[playername] -- F10/Carrier Trainer/ - local _trainPath = missionCommands.addSubMenuForGroup(_gid, self.alias, CARRIERTRAINER.MenuF10[_gid]) + local _trainPath = missionCommands.addSubMenuForGroup(_gid, self.alias, CARRIERTRAINER.MenuF10[_gid]) -- F10/Carrier Trainer//Results - local _statsPath = missionCommands.addSubMenuForGroup(_gid, "LSO Grades", _trainPath) + local _statsPath = missionCommands.addSubMenuForGroup(_gid, "LSO Grades", _trainPath) -- F10/Carrier Trainer//My Settings/Difficulty - local _difficulPath = missionCommands.addSubMenuForGroup(_gid, "Difficulty", _trainPath) + local _difficulPath = missionCommands.addSubMenuForGroup(_gid, "Difficulty", _trainPath) -- F10/Carrier Trainer//Results/ - -- TODO: Add result functions. missionCommands.addCommandForGroup(_gid, "Greenie Board", _statsPath, self._DisplayScoreBoard, self, _unitName) missionCommands.addCommandForGroup(_gid, "My Grades", _statsPath, self._DisplayPlayerGrades, self, _unitName) --missionCommands.addCommandForGroup(_gid, "(Clear ALL Results)", _statsPath, self._ResetRangeStats, self, _unitName) @@ -2367,11 +2364,12 @@ function CARRIERTRAINER:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _difficulPath, self._SetDifficulty, self, playername, CARRIERTRAINER.Difficulty.HARD) -- F10/Carrier Trainer// - missionCommands.addCommandForGroup(_gid, "Carrier Info", _trainPath, self._DisplayCarrierInfo, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Weather Report", _trainPath, self._DisplayCarrierWeather, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Carrier Info", _trainPath, self._DisplayCarrierInfo, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Weather Report", _trainPath, self._DisplayCarrierWeather, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _trainPath, self._AttitudeMonitor, self, playername) --TODO: Flare carrier. - missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _trainPath, self._AttitudeMonitor, self, playername) + end else self:T(self.lid.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) @@ -2396,7 +2394,10 @@ function CARRIERTRAINER:_DisplayPlayerGrades(_unitName) local playerData=self.players[_playername] --#CARRIERTRAINTER.PlayerData if playerData then + + -- Grades of player: local text=string.format("Your grades, %s:", _playername) + local p=0 for i,_grade in pairs(playerData.grades) do local grade=_grade --#CARRIERTRAINER.LSOgrade @@ -2411,15 +2412,15 @@ function CARRIERTRAINER:_DisplayPlayerGrades(_unitName) if n>0 then text=text..string.format("\nAverage points = %.1f", p/n) else - text=text..string.format("No data available.") + text=text..string.format("\nNo data available.") end + env.info("FF:\n"..text) + -- Send message. if playerData.client then - --MESSAGE:New(string.format("%s, %s, ", self.alias, playerData.callsign)..message, duration, nil, clear):ToClient(playerData.client) - end - MESSAGE:New(text, 30, nil, true):ToAll() - + MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) + end end end end @@ -2439,52 +2440,40 @@ function CARRIERTRAINER:_DisplayScoreBoard(_unitName) -- Results table. local _playerResults={} + + -- Player data of requestor. + local playerData=self.players[_playername] --#CARRIERTRAINER.PlayerData -- Message text. - local _message = string.format("Greenie Board:\n") - - -- Loop over player results. - for _playerName,_grades in pairs(self.playerGrades) do - - - -- Get the best result of the player. - local _best=nil - for _,_grade in pairs(_grades) do - local grade=_grade --#CARRIERTRAINTER.LSOgrade - if _best==nil or grade.po > _best.hits then - _best = _result - end + local text = string.format("Greenie Board:") + + for _playerName,_playerData in pairs(self.players) do + + local Paverage=0 + for _,_grade in pairs(_playerData.grades) do + Paverage=Paverage+_grade.points end - - -- Add best result to table. - if _best ~= nil then - local text=string.format("%s: Hits %i - %s - %s", _playerName, _best.hits, _best.zone.name, _best.text) - table.insert(_playerResults,{msg = text, hits = _best.hits}) - end - + _playerResults[_playerName]=Paverage + end - + --Sort list! - local _sort=function(a, b) return a.points>b.points end - table.sort(self.playerGrades,_sort) + local _sort=function(a, b) return a>b end + table.sort(_playerResults,_sort) - local _i=0 - for _playername,_grade in pairs(self.playerGrades) do - _message=_message..string.format("\n[%d] %s", _i, _grade) - end - - -- Add top 10 results. - for _i = 1, 10 do --math.min(#_playerResults, self.ndisplayresult) do - _message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg) + local i=1 + for _playerName,_points in pairs(_playerResults) do + text=text..string.format("\n[%d] %.1f %s", i,_points,_playerName) + i=i+1 end - -- In case there are no scores yet. - if #_playerResults<1 then - _message = _message.."No player scored yet." - end - + env.info("FF:\n"..text) + -- Send message. - self:_DisplayMessageToGroup(_unit, _message, nil, true) + if playerData.client then + MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) + end + end end From e7181c255b1fd5b2f6c370aca4d6ffcc0c5c908b Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 8 Nov 2018 07:38:04 +0100 Subject: [PATCH 020/147] Got engage working from the air. Now I need to fix: - Overhead - Correct Defense Type assignment to engage. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 59 ++++++++++++------- Moose Development/Moose/AI/AI_A2G_Patrol.lua | 2 +- Moose Development/Moose/Core/Zone.lua | 28 +++++++-- 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 3eacf59fb..33b855f9c 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -325,53 +325,53 @@ do -- AI_A2G_DISPATCHER self:AddTransition( "*", "Defend", "*" ) - --- GCI Handler OnBefore for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeGCI + --- Defend Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeDefend -- @param #AI_A2G_DISPATCHER self -- @param #string From -- @param #string Event -- @param #string To -- @return #boolean - --- GCI Handler OnAfter for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] OnAfterGCI + --- Defend Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterDefend -- @param #AI_A2G_DISPATCHER self -- @param #string From -- @param #string Event -- @param #string To - --- GCI Trigger for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] GCI + --- Defend Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Defend -- @param #AI_A2G_DISPATCHER self - --- GCI Asynchronous Trigger for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] __GCI + --- Defend Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Defend -- @param #AI_A2G_DISPATCHER self -- @param #number Delay - self:AddTransition( "*", "ENGAGE", "*" ) + self:AddTransition( "*", "Engage", "*" ) - --- ENGAGE Handler OnBefore for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeENGAGE + --- Engage Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeEngage -- @param #AI_A2G_DISPATCHER self -- @param #string From -- @param #string Event -- @param #string To -- @return #boolean - --- ENGAGE Handler OnAfter for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] OnAfterENGAGE + --- Engage Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterEngage -- @param #AI_A2G_DISPATCHER self -- @param #string From -- @param #string Event -- @param #string To - --- ENGAGE Trigger for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] ENGAGE + --- Engage Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Engage -- @param #AI_A2G_DISPATCHER self - --- ENGAGE Asynchronous Trigger for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] __ENGAGE + --- Engage Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Engage -- @param #AI_A2G_DISPATCHER self -- @param #number Delay @@ -618,7 +618,7 @@ do -- AI_A2G_DISPATCHER -- function AI_A2G_DISPATCHER:SetDefenseRadius( DefenseRadius ) - self.DefenseRadius = DefenseRadius or 40000 + self.DefenseRadius = DefenseRadius or 100000 return self end @@ -765,7 +765,24 @@ do -- AI_A2G_DISPATCHER -- @return #table A list of the defender friendlies nearby, sorted by distance. function AI_A2G_DISPATCHER:GetDefenderFriendliesNearBy( DetectedItem ) - local DefenderFriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem ) +-- local DefenderFriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem ) + + local DefenderFriendliesNearBy = {} + + local DetectionCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local ScanZone = ZONE_RADIUS:New( "ScanZone", DetectionCoordinate:GetVec2(), self.DefenseRadius ) + + ScanZone:Scan( Object.Category.UNIT, { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + + local DefenderUnits = ScanZone:GetScannedUnits() + + for DefenderUnitID, DefenderUnit in pairs( DefenderUnits ) do + local DefenderUnit = UNIT:FindByName( DefenderUnit:getName() ) + + DefenderFriendliesNearBy[#DefenderFriendliesNearBy+1] = DefenderUnit + end + return DefenderFriendliesNearBy end @@ -1154,7 +1171,7 @@ do -- AI_A2G_DISPATCHER if DefenderSquadron.Captured == false then -- We can only spawn new defense if the home airbase has not been captured. if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. - if DefenderSquadron[DefenseTaskType] and DefenderSquadron[DefenseTaskType].Defend == true then + if DefenderSquadron[DefenseTaskType] and ( DefenderSquadron[DefenseTaskType].Defend == true ) then return DefenderSquadron, DefenderSquadron[DefenseTaskType] end end @@ -2381,7 +2398,7 @@ do -- AI_A2G_DISPATCHER if Defenders then - for DefenderID, Defender in pairs( Defenders ) do + for DefenderID, Defender in pairs( Defenders or {} ) do local Fsm = self:GetDefenderTaskFsm( Defender ) Fsm:__Engage( 1, AttackerDetection.Set ) -- Engage on the TargetSetUnit diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua index dcf4b2acf..a21929d03 100644 --- a/Moose Development/Moose/AI/AI_A2G_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2G_Patrol.lua @@ -417,7 +417,7 @@ function AI_A2G_PATROL:onafterEngage( AIPatrol, From, Event, To, AttackSetUnit ) for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT self:T( { "Attacking Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) - if AttackUnit:IsAlive() and AttackUnit:IsAir() then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then AttackTasks[#AttackTasks+1] = AIPatrol:TaskAttackUnit( AttackUnit ) end end diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 244d5de66..9b5b6827f 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -618,6 +618,9 @@ function ZONE_RADIUS:GetVec3( Height ) end + + + --- Scan the zone for the presence of units of the given ObjectCategories. -- Note that after a zone has been scanned, the zone can be evaluated by: -- @@ -629,11 +632,11 @@ end -- @{#ZONE_RADIUS. -- @param #ZONE_RADIUS self -- @param ObjectCategories --- @param Coalition +-- @param UnitCategories -- @usage -- self.Zone:Scan() -- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) -function ZONE_RADIUS:Scan( ObjectCategories ) +function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) self.ScanData = {} self.ScanData.Coalitions = {} @@ -660,9 +663,24 @@ function ZONE_RADIUS:Scan( ObjectCategories ) if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then local CoalitionDCSUnit = ZoneObject:getCoalition() - self.ScanData.Coalitions[CoalitionDCSUnit] = true - self.ScanData.Units[ZoneObject] = ZoneObject - self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) + local Include = false + if not UnitCategories then + Include = true + else + local CategoryDCSUnit = ZoneObject:getDesc().category + for UnitCategoryID, UnitCategory in pairs( UnitCategories ) do + if UnitCategory == CategoryDCSUnit then + Include = true + break + end + end + end + if Include then + local CoalitionDCSUnit = ZoneObject:getCoalition() + self.ScanData.Coalitions[CoalitionDCSUnit] = true + self.ScanData.Units[ZoneObject] = ZoneObject + self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) + end end if ObjectCategory == Object.Category.SCENERY then local SceneryType = ZoneObject:getTypeName() From b53b1edd29335c8d5819032619d420ca52360d02 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 8 Nov 2018 16:04:51 +0100 Subject: [PATCH 021/147] CT v0.2.0w --- Moose Development/Moose/Core/Radio.lua | 97 ++++++++++++++- .../Moose/Functional/CarrierTrainer.lua | 112 ++++++++++++++---- 2 files changed, 180 insertions(+), 29 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 954fc51a9..3810e18e3 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -364,22 +364,28 @@ end -- Use @{#BEACON:StopRadioBeacon}() to stop it. -- -- @type BEACON +-- @field #string ClassName Name of the class "BEACON". +-- @field Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. -- @extends Core.Base#BASE BEACON = { ClassName = "BEACON", + Positionable = nil, } ---- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.AATACAN} or @{#BEACON.Generic} +--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.AATACAN} or @{#BEACON.Generic}. -- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead. -- @param #BEACON self -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #BEACON Beacon --- @return #nil If Positionable is invalid +-- @return #BEACON Beacon or #nil if Positionable is invalid. function BEACON:New(Positionable) - local self = BASE:Inherit(self, BASE:New()) + + -- Inherit BASE. + local self = BASE:Inherit(self, BASE:New()) --#BEACON + -- Debug. self:F(Positionable) + -- Set positionable. if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid self.Positionable = Positionable return self @@ -428,6 +434,84 @@ function BEACON:_TACANToFrequency(TACANChannel, TACANMode) return (A + TACANChannel - B) * 1000000 end +--- Activates a TACAN BEACON. +-- @param #BEACON self +-- @param #number TACANChannel TACAN channel, i.e. the "10" part in "10Y". +-- @param #string TACANMode TACAN mode, i.e. the "Y" part in "10Y". Note that AA TACAN are only available on Y Channels. +-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available. +-- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a TACAN Beacon for a tanker +-- local myUnit = UNIT:FindByName("MyUnit") +-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon +-- +-- myBeacon:TACAN(20, "Y", "TEXACO", true) -- Activate the beacon +function BEACON:TACAN(TACANChannel, TACANMode, Message, Bearing, BeaconDuration) + self:F({TACANChannel, Message, Bearing, BeaconDuration}) + + -- Get frequency. + local Frequency = self:_TACANToFrequency(TACANChannel, TACANMode) + + -- Check. + if not Frequency then + self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) + return self + end + + if self.Positionable:IsAir() then + --TODO: set TACANMode="Y" + self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft ! The BEACON is not emitting", self.Positionable}) + end + + + -- Using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the beacon shows its bearing or 14 (TACAN_AA_MODE_Y) if it does not. + local System=14 + if Bearing then + System = 5 + end + + -- Beacon command https://wiki.hoggitworld.com/view/DCS_command_activateBeacon + local beaconcommand={ + id = "ActivateBeacon", + params = { + type = 4, --BEACON_TYPE_TACAN + system = System, + callsign = Message, + frequency = Frequency, + } + } + + -- Debug + self:T2({"TACAN BEACON started!"}) + + -- Start beacon. + self.Positionable:SetCommand(beaconcommand) + + -- Stop sheduler + if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD + SCHEDULER:New(self, self.StopTACAN, {self}, BeaconDuration) + end + + return self +end + +--- Stops the TACAN BEACON. +-- @param #BEACON self +-- @return #BEACON self +function BEACON:StopTACAN() + self:F() + if self.Positionable==nil then + self:E({"Start the beacon first before stoping it !"}) + else + local commandstop={id='DeactivateBeacon', params={}} + self.Positionable:SetCommand(commandstop) + end + return self +end + + --- Activates a TACAN BEACON on an Aircraft. -- @param #BEACON self @@ -591,4 +675,7 @@ function BEACON:StopRadioBeacon() self:F() -- The unique name of the transmission is the class ID trigger.action.stopRadioTransmission(tostring(self.ID)) -end \ No newline at end of file + return self +end + + diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 139b736be..3693ab24d 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -27,6 +27,11 @@ -- @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.Radio#BEACON beacon Carrier beacon for TACAN and ICLS. +-- @field #number TACANchannel TACAN channel. +-- @field #string TACANmode TACAN mode, i.e. "X" or "Y". +-- @field Core.Radio#RADIO LSOradio Radio for LSO calls. +-- @field Core.Radio#RADIO Carrierradio Radio for carrier calls. -- @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. @@ -63,6 +68,14 @@ CARRIERTRAINER = { carrier = nil, carriertype = nil, alias = nil, + beacon = nil, + TACANchannel = nil, + TACANmode = nil, + ICLS = nil, + LSOradio = nil, + LSOfreq = nil, + Carrierradio = nil, + Carrierfreq = nil, registerZone = nil, startZone = nil, giantZone = nil, @@ -76,8 +89,6 @@ CARRIERTRAINER = { Wake = {}, Groove = {}, Trap = {}, - TACAN = nil, - ICLS = nil, rwyangle = -10, sterndist =-100, deckheight = 22, @@ -264,15 +275,15 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.2.0" +CARRIERTRAINER.version="0.2.0w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Add scoring to radio menu. --- TODO: Optimized debrief. --- TODO: Add automatic grading. +-- DONE: Add scoring to radio menu. +-- DONE: Optimized debrief. +-- DONE: Add automatic grading. -- TODO: Get board numbers. -- TODO: Get fuel state in pounds. -- TODO: Add user functions. @@ -312,7 +323,7 @@ function CARRIERTRAINER:New(carriername, alias) self:E(text) return nil end - + -- Set some string id for output to DCS.log file. self.lid=string.format("CARRIERTRAINER %s | ", carriername) @@ -322,6 +333,16 @@ function CARRIERTRAINER:New(carriername, alias) -- Set alias. self.alias=alias or carriername + -- Get carrier group template. + local grouptemplate=self.carrier:GetGroup():GetTemplate() + -- TODO: Now I need to get TACAN and ICLS if they were set in the ME. + + -- Create carrier beacon. + self.beacon=BEACON:New(self.carrier) + + self.Carrierradio=RADIO:New(self.carrier) + self.LSOradio=RADIO:New(self.carrier) + if self.carriertype==CARRIERTRAINER.CarrierType.STENNIS then self:_InitStennis() elseif self.carriertype==CARRIERTRAINER.CarrierType.VINSON then @@ -377,6 +398,41 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Set TACAN channel of carrier. +-- @param #CARRIERTRAINER self +-- @param #number channel TACAN channel. +-- @param #string mode TACAN mode, i.e. "X" or "Y". +-- @return #CARRIERTRAINER self +function CARRIERTRAINER:SetTACAN(channel, mode) + + self.TACANchannel=channel + self.TACANmode=mode or "X" + + return self +end + + +--- Set LSO radio frequency. +-- @param #CARRIERTRAINER self +-- @param #number freq Frequency in MHz. +-- @return #CARRIERTRAINER self +function CARRIERTRAINER:SetLSOradio(freq) + + self.LSOfreq=freq + + return self +end + +--- Set carrier radio frequency. +-- @param #CARRIERTRAINER self +-- @param #number freq Frequency in MHz. +-- @return #CARRIERTRAINER self +function CARRIERTRAINER:SetCarrierradio(freq) + + self.Carrierfreq=freq + + return self +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM states @@ -392,9 +448,13 @@ 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)) + -- Activate TACAN. + self.beacon:TACAN(self.TACANchannel, self.TACANmode, "STN", true, nil) + -- Handle events. self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Land) + --self:HandleEvent(EVENTS.Crash) -- Init status check self:__Status(1) @@ -672,7 +732,6 @@ end function CARRIERTRAINER:_InitNewRound(playerData) self:I(self.lid..string.format("New round for player %s.", playerData.callsign)) playerData.step=0 - playerData.score=100 playerData.groove={} playerData.debrief={} playerData.patternwo=false @@ -1027,17 +1086,17 @@ function CARRIERTRAINER:_Groove(playerData) -- Gather pilot data. local groovedata={} --#CARRIERTRAINER.GrooveData + groovedata.Step=playerData.step groovedata.Alt=alt groovedata.AoA=aoa groovedata.GSE=self:_Glideslope(playerData)-3.5 groovedata.LUE=self:_Lineup(playerData)-self.rwyangle groovedata.Roll=roll - groovedata.Step=playerData.step - + -- Groove playerData.groove.X0=groovedata - -- Next step: X call the ball. + -- Next step: X start & call the ball. playerData.step=91 end @@ -1119,7 +1178,7 @@ function CARRIERTRAINER:_CallTheBall(playerData) elseif rho<=RIM and playerData.step==93 then - --TODO: grade for IM + -- Debug. self:_SendMessageToPlayer("IM", 8, playerData) env.info(string.format("FF IM=%d", rho)) @@ -1131,8 +1190,10 @@ function CARRIERTRAINER:_CallTheBall(playerData) elseif rho<=RIC and playerData.step==94 then + -- Check if player was already waved off. if playerData.waveoff==false then + -- Debug self:_SendMessageToPlayer("IC", 8, playerData) env.info(string.format("FF IC=%d", rho)) @@ -1155,7 +1216,7 @@ function CARRIERTRAINER:_CallTheBall(playerData) return else - -- Next step: at the ramp. + -- Next step: AR at the ramp. playerData.step=95 end @@ -1163,7 +1224,7 @@ function CARRIERTRAINER:_CallTheBall(playerData) elseif rho<=RAR and playerData.step==95 then - --TODO: grade for AR + -- Debug. self:_SendMessageToPlayer("AR", 8, playerData) env.info(string.format("FF AR=%d", rho)) @@ -1235,8 +1296,8 @@ function CARRIERTRAINER:_CheckWaveOff(glideslopeError, lineupError, AoA) -- Too slow or too fast? if AoA<6.9 or AoA>9.3 then - self:I(self.lid.."Wave off due to AoA<6.9 or AoA>9.3!") - waveoff=true + self:I(self.lid.."DEACTIVE! Wave off due to AoA<6.9 or AoA>9.3!") + waveoff=false end return waveoff @@ -1251,7 +1312,7 @@ function CARRIERTRAINER:_GS(step) if step==90 then gp="X0" -- Entering the groove. elseif step==91 then - gp="XX" -- Starting the groove. + gp="X" -- Starting the groove. elseif step==92 then gp="RB" -- Roger ball call. elseif step==93 then @@ -1490,9 +1551,10 @@ function CARRIERTRAINER:_Debrief(playerData) -- New approach. if playerData.boltered or playerData.waveoff or playerData.patternwo then + -- Get heading and distance to register zone ~3 NM astern. local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) - local text=string.format("fly heading %d for %d NM to restart the pattern.", heading, UTILS.MetersToNM(distance)) + local text=string.format("fly heading %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) self:_SendMessageToPlayer(text, 10, playerData, false, nil, 30) end @@ -1684,9 +1746,7 @@ function CARRIERTRAINER:_AbortPattern(playerData, X, Z, posData) -- Pattern wave off! playerData.patternwo=true - - --TODO: set score and grade. - + -- Next step debrief. playerData.step=999 end @@ -1743,9 +1803,13 @@ end function CARRIERTRAINER:_InitStennis() -- Carrier Parameters. - self.rwyangle = -10 - self.sterndist =-150 - self.deckheight = 22 + self.rwyangle = -10 + self.sterndist =-150 + self.deckheight = 22 + self.wire1 =-100 + self.wire2 =-90 + self.wire3 =-80 + self.wire4 =-70 --[[ q0=self.carrier:GetCoordinate():SetAltitude(25) From 6fac8fe94031ffaba67bf84b73224f2c6aaf1823 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 9 Nov 2018 00:17:57 +0100 Subject: [PATCH 022/147] CT v0.2.1 --- Moose Development/Moose/Functional/CarrierTrainer.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 3693ab24d..4fed798e9 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -275,7 +275,7 @@ CARRIERTRAINER.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.2.0w" +CARRIERTRAINER.version="0.2.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1297,7 +1297,7 @@ function CARRIERTRAINER:_CheckWaveOff(glideslopeError, lineupError, AoA) -- Too slow or too fast? if AoA<6.9 or AoA>9.3 then self:I(self.lid.."DEACTIVE! Wave off due to AoA<6.9 or AoA>9.3!") - waveoff=false + --waveoff=true end return waveoff From 488804308b2d974bee2f174a717d4826f4cd702c Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 9 Nov 2018 06:27:16 +0100 Subject: [PATCH 023/147] Undo my last stupid mistakes. --- Moose Development/Moose/AI/AI_A2G.lua | 69 ++ Moose Development/Moose/AI/AI_A2G_Engage.lua | 440 +++++++++++ Moose Development/Moose/AI/AI_Air.lua | 732 +++++++++++++++++++ 3 files changed, 1241 insertions(+) create mode 100644 Moose Development/Moose/AI/AI_A2G.lua create mode 100644 Moose Development/Moose/AI/AI_A2G_Engage.lua create mode 100644 Moose Development/Moose/AI/AI_Air.lua diff --git a/Moose Development/Moose/AI/AI_A2G.lua b/Moose Development/Moose/AI/AI_A2G.lua new file mode 100644 index 000000000..2f6a8f500 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G.lua @@ -0,0 +1,69 @@ +--- **AI** -- Models the process of air to ground operations for airplanes and helicopters. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G +-- @image AI_Air_To_Ground_Dispatching.JPG + +--- @type AI_A2G +-- @extends AI.AI_Air#AI_AIR + +--- The AI_A2G class implements the core functions to operate an AI @{Wrapper.Group} A2G tasking. +-- +-- +-- # 1) AI_A2G constructor +-- +-- * @{#AI_A2G.New}(): Creates a new AI_A2G object. +-- +-- # 2) AI_A2G is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- +-- ## 2.1) AI_A2G States. +-- +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_A2G Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- @field #AI_A2G +AI_A2G = { + ClassName = "AI_A2G", +} + +--- Creates a new AI_A2G process. +-- @param #AI_A2G self +-- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process. +-- @return #AI_A2G +function AI_A2G:New( AIGroup ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_AIR:New( AIGroup ) ) -- #AI_A2G + + self:SetFuelThreshold( .2, 60 ) + self:SetDamageThreshold( 0.4 ) + self:SetDisengageRadius( 70000 ) + + return self +end + diff --git a/Moose Development/Moose/AI/AI_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua new file mode 100644 index 000000000..26e993420 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_Engage.lua @@ -0,0 +1,440 @@ +--- **AI** -- Models the process of air to ground engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_ENGAGE +-- @image AI_Ground_Control_Engage.JPG + + + +--- @type AI_A2G_ENGAGE +-- @extends AI.AI_A2A#AI_A2A + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- ![Process](..\Presentations\AI_GCI\Dia3.JPG) +-- +-- The AI_A2G_ENGAGE is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_ENGAGE process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_GCI\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_GCI\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_GCI\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_GCI\Dia13.JPG) +-- +-- ## 1. AI_A2G_ENGAGE constructor +-- +-- * @{#AI_A2G_ENGAGE.New}(): Creates a new AI_A2G_ENGAGE object. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_GCI\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_GCI#AI_A2G_ENGAGE.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_A2G_ENGAGE.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2G_ENGAGE +AI_A2G_ENGAGE = { + ClassName = "AI_A2G_ENGAGE", +} + + + +--- Creates a new AI_A2G_ENGAGE object +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup +-- @return #AI_A2G_ENGAGE +function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G:New( AIGroup ) ) -- #AI_A2G_ENGAGE + + self.Accomplished = false + self.Engaging = false + + self.EngageMinSpeed = EngageMinSpeed + self.EngageMaxSpeed = EngageMaxSpeed + self.PatrolMinSpeed = EngageMinSpeed + self.PatrolMaxSpeed = EngageMaxSpeed + + self.PatrolAltType = "RADIO" + + self:AddTransition( { "Started", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeEngage + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterEngage + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] Engage + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] __Engage + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_A2G_ENGAGE] OnLeaveEngaging +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_A2G_ENGAGE] OnEnterEngaging +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeFired + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterFired + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] Fired + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] __Fired + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeDestroy + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterDestroy + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] Destroy + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] __Destroy + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeAbort + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterAbort + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] Abort + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] __Abort + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeAccomplish + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterAccomplish + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] Accomplish + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] __Accomplish + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + return self +end + +--- onafter event handler for Start event. +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterStart( AIGroup, From, Event, To ) + + self:GetParent( self ).onafterStart( self, AIGroup, From, Event, To ) + AIGroup:HandleEvent( EVENTS.Takeoff, nil, self ) + +end + + + +--- onafter event handler for Engage event. +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterEngage( AIGroup, From, Event, To ) + + self:HandleEvent( EVENTS.Dead ) + +end + +-- todo: need to fix this global function + +--- @param Wrapper.Group#GROUP AIControllable +function AI_A2G_ENGAGE.EngageRoute( AIGroup, Fsm ) + + AIGroup:F( { "AI_A2G_ENGAGE.EngageRoute:", AIGroup:GetName() } ) + + if AIGroup:IsAlive() then + Fsm:__Engage( 0.5 ) + + --local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) + --AIGroup:SetTask( Task ) + end +end + +--- onbefore event handler for Engage event. +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onbeforeEngage( AIGroup, From, Event, To ) + + if self.Accomplished == true then + return false + end +end + +--- onafter event handler for Abort event. +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterAbort( AIGroup, From, Event, To ) + AIGroup:ClearTasks() + self:Return() + self:__RTB( 0.5 ) +end + + +--- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The GroupGroup managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterEngage( AIGroup, From, Event, To, AttackSetUnit ) + + self:F( { AIGroup, From, Event, To, AttackSetUnit} ) + + self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + + local FirstAttackUnit = self.AttackSetUnit:GetFirst() + + if FirstAttackUnit and FirstAttackUnit:IsAlive() then + + if AIGroup:IsAlive() then + + local EngageRoute = {} + + local CurrentCoord = AIGroup:GetCoordinate() + + --- Calculate the target route point. + + local CurrentCoord = AIGroup:GetCoordinate() + + local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + self:SetTargetDistance( ToTargetCoord ) -- For RTB status check + + local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + local ToEngageAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) + + --- Create a route point of type air. + local ToPatrolRoutePoint = CurrentCoord:Translate( 15000, ToEngageAngle ):WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = ToEngageAngle, ToTargetSpeed = ToTargetSpeed } ) + self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint + EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint + + local AttackTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "Eliminating Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) + AttackTasks[#AttackTasks+1] = AIGroup:TaskAttackUnit( AttackUnit ) + end + end + + if #AttackTasks == 0 then + self:E("No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + else + AIGroup:OptionROEOpenFire() + AIGroup:OptionROTEvadeFire() + + AttackTasks[#AttackTasks+1] = AIGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( AttackTasks ) + end + + AIGroup:Route( EngageRoute, 0.5 ) + + end + else + self:E("No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + end +end + +--- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterAccomplish( AIGroup, From, Event, To ) + self.Accomplished = true + self:SetDetectionOff() +end + +--- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_A2G_ENGAGE:onafterDestroy( AIGroup, From, Event, To, EventData ) + + if EventData.IniUnit then + self.AttackUnits[EventData.IniUnit] = nil + end +end + +--- @param #AI_A2G_ENGAGE self +-- @param Core.Event#EVENTDATA EventData +function AI_A2G_ENGAGE:OnEventDead( EventData ) + self:F( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then + self:__Destroy( 1, EventData ) + end + end +end diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua new file mode 100644 index 000000000..80a58bf7f --- /dev/null +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -0,0 +1,732 @@ +--- **AI** -- Models the process of AI air operations. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Air +-- @image AI_Air_Operations.JPG + +--- @type AI_AIR +-- @extends Core.Fsm#FSM_CONTROLLABLE + +--- The AI_AIR class implements the core functions to operate an AI @{Wrapper.Group}. +-- +-- +-- # 1) AI_AIR constructor +-- +-- * @{#AI_AIR.New}(): Creates a new AI_AIR object. +-- +-- # 2) AI_AIR is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- +-- ## 2.1) AI_AIR States. +-- +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_AIR Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- @field #AI_AIR +AI_AIR = { + ClassName = "AI_AIR", +} + +--- Creates a new AI_AIR process. +-- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process. +-- @return #AI_AIR +function AI_AIR:New( AIGroup ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_AIR + + self:SetControllable( AIGroup ) + + self:SetStartState( "Stopped" ) + + self:AddTransition( "*", "Start", "Started" ) + + --- Start Handler OnBefore for AI_AIR + -- @function [parent=#AI_AIR] OnBeforeStart + -- @param #AI_AIR self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Start Handler OnAfter for AI_AIR + -- @function [parent=#AI_AIR] OnAfterStart + -- @param #AI_AIR self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Start Trigger for AI_AIR + -- @function [parent=#AI_AIR] Start + -- @param #AI_AIR self + + --- Start Asynchronous Trigger for AI_AIR + -- @function [parent=#AI_AIR] __Start + -- @param #AI_AIR self + -- @param #number Delay + + self:AddTransition( "*", "Stop", "Stopped" ) + +--- OnLeave Transition Handler for State Stopped. +-- @function [parent=#AI_AIR] OnLeaveStopped +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Stopped. +-- @function [parent=#AI_AIR] OnEnterStopped +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- OnBefore Transition Handler for Event Stop. +-- @function [parent=#AI_AIR] OnBeforeStop +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Stop. +-- @function [parent=#AI_AIR] OnAfterStop +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Stop. +-- @function [parent=#AI_AIR] Stop +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event Stop. +-- @function [parent=#AI_AIR] __Stop +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. + +--- OnBefore Transition Handler for Event Status. +-- @function [parent=#AI_AIR] OnBeforeStatus +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Status. +-- @function [parent=#AI_AIR] OnAfterStatus +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Status. +-- @function [parent=#AI_AIR] Status +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event Status. +-- @function [parent=#AI_AIR] __Status +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "RTB", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. + +--- OnBefore Transition Handler for Event RTB. +-- @function [parent=#AI_AIR] OnBeforeRTB +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event RTB. +-- @function [parent=#AI_AIR] OnAfterRTB +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event RTB. +-- @function [parent=#AI_AIR] RTB +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event RTB. +-- @function [parent=#AI_AIR] __RTB +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Returning. +-- @function [parent=#AI_AIR] OnLeaveReturning +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Returning. +-- @function [parent=#AI_AIR] OnEnterReturning +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Patrolling", "Refuel", "Refuelling" ) + + --- Refuel Handler OnBefore for AI_AIR + -- @function [parent=#AI_AIR] OnBeforeRefuel + -- @param #AI_AIR self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Refuel Handler OnAfter for AI_AIR + -- @function [parent=#AI_AIR] OnAfterRefuel + -- @param #AI_AIR self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Refuel Trigger for AI_AIR + -- @function [parent=#AI_AIR] Refuel + -- @param #AI_AIR self + + --- Refuel Asynchronous Trigger for AI_AIR + -- @function [parent=#AI_AIR] __Refuel + -- @param #AI_AIR self + -- @param #number Delay + + self:AddTransition( "*", "Takeoff", "Airborne" ) + self:AddTransition( "*", "Return", "Returning" ) + self:AddTransition( "*", "Hold", "Holding" ) + self:AddTransition( "*", "Home", "Home" ) + self:AddTransition( "*", "LostControl", "LostControl" ) + self:AddTransition( "*", "Fuel", "Fuel" ) + self:AddTransition( "*", "Damaged", "Damaged" ) + self:AddTransition( "*", "Eject", "*" ) + self:AddTransition( "*", "Crash", "Crashed" ) + self:AddTransition( "*", "PilotDead", "*" ) + + self.IdleCount = 0 + + return self +end + +--- @param Wrapper.Group#GROUP self +-- @param Core.Event#EVENTDATA EventData +function GROUP:OnEventTakeoff( EventData, Fsm ) + Fsm:Takeoff() + self:UnHandleEvent( EVENTS.Takeoff ) +end + + + +function AI_AIR:SetDispatcher( Dispatcher ) + self.Dispatcher = Dispatcher +end + +function AI_AIR:GetDispatcher() + return self.Dispatcher +end + +function AI_AIR:SetTargetDistance( Coordinate ) + + local CurrentCoord = self.Controllable:GetCoordinate() + self.TargetDistance = CurrentCoord:Get2DDistance( Coordinate ) + + self.ClosestTargetDistance = ( not self.ClosestTargetDistance or self.ClosestTargetDistance > self.TargetDistance ) and self.TargetDistance or self.ClosestTargetDistance +end + + +function AI_AIR:ClearTargetDistance() + + self.TargetDistance = nil + self.ClosestTargetDistance = nil +end + + +--- Sets (modifies) the minimum and maximum speed of the patrol. +-- @param #AI_AIR self +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @return #AI_AIR self +function AI_AIR:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) + self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) + + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed +end + + +--- Sets the floor and ceiling altitude of the patrol. +-- @param #AI_AIR self +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @return #AI_AIR self +function AI_AIR:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) + self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) + + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude +end + + +--- Sets the home airbase. +-- @param #AI_AIR self +-- @param Wrapper.Airbase#AIRBASE HomeAirbase +-- @return #AI_AIR self +function AI_AIR:SetHomeAirbase( HomeAirbase ) + self:F2( { HomeAirbase } ) + + self.HomeAirbase = HomeAirbase +end + +--- Sets to refuel at the given tanker. +-- @param #AI_AIR self +-- @param Wrapper.Group#GROUP TankerName The group name of the tanker as defined within the Mission Editor or spawned. +-- @return #AI_AIR self +function AI_AIR:SetTanker( TankerName ) + self:F2( { TankerName } ) + + self.TankerName = TankerName +end + + +--- Sets the disengage range, that when engaging a target beyond the specified range, the engagement will be cancelled and the plane will RTB. +-- @param #AI_AIR self +-- @param #number DisengageRadius The disengage range. +-- @return #AI_AIR self +function AI_AIR:SetDisengageRadius( DisengageRadius ) + self:F2( { DisengageRadius } ) + + self.DisengageRadius = DisengageRadius +end + +--- Set the status checking off. +-- @param #AI_AIR self +-- @return #AI_AIR self +function AI_AIR:SetStatusOff() + self:F2() + + self.CheckStatus = false +end + + +--- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. +-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. +-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_AIR. +-- Once the time is finished, the old AI will return to the base. +-- @param #AI_AIR self +-- @param #number FuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. +-- @param #number OutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. +-- @return #AI_AIR self +function AI_AIR:SetFuelThreshold( FuelThresholdPercentage, OutOfFuelOrbitTime ) + + self.FuelThresholdPercentage = FuelThresholdPercentage + self.OutOfFuelOrbitTime = OutOfFuelOrbitTime + + self.Controllable:OptionRTBBingoFuel( false ) + + return self +end + +--- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. +-- However, damage cannot be foreseen early on. +-- Therefore, when the damage treshold is reached, +-- the AI will return immediately to the home base (RTB). +-- Note that for groups, the average damage of the complete group will be calculated. +-- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. +-- @param #AI_AIR self +-- @param #number PatrolDamageThreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. +-- @return #AI_AIR self +function AI_AIR:SetDamageThreshold( PatrolDamageThreshold ) + + self.PatrolManageDamage = true + self.PatrolDamageThreshold = PatrolDamageThreshold + + return self +end + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_AIR self +-- @return #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR:onafterStart( Controllable, From, Event, To ) + + self:__Status( 10 ) -- Check status status every 30 seconds. + + self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) + self:HandleEvent( EVENTS.Crash, self.OnCrash ) + self:HandleEvent( EVENTS.Ejection, self.OnEjection ) + + Controllable:OptionROEHoldFire() + Controllable:OptionROTVertical() +end + + + +--- @param #AI_AIR self +function AI_AIR:onbeforeStatus() + + return self.CheckStatus +end + +--- @param #AI_AIR self +function AI_AIR:onafterStatus() + + if self.Controllable and self.Controllable:IsAlive() then + + local RTB = false + + local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) + + if not self:Is( "Holding" ) and not self:Is( "Returning" ) then + local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) + self:F({DistanceFromHomeBase=DistanceFromHomeBase}) + + if DistanceFromHomeBase > self.DisengageRadius then + self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) + self:Hold( 300 ) + RTB = false + end + end + +-- I think this code is not requirement anymore after release 2.5. +-- if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then +-- if DistanceFromHomeBase < 5000 then +-- self:E( self.Controllable:GetName() .. " is near the home base, RTB!" ) +-- self:Home( "Destroy" ) +-- end +-- end + + + if not self:Is( "Fuel" ) and not self:Is( "Home" ) then + local Fuel = self.Controllable:GetFuelMin() + self:F({Fuel=Fuel, FuelThresholdPercentage=self.FuelThresholdPercentage}) + if Fuel < self.FuelThresholdPercentage then + if self.TankerName then + self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) + self:Refuel() + else + self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... RTB!" ) + local OldAIControllable = self.Controllable + + local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.OutOfFuelOrbitTime,nil ) ) + OldAIControllable:SetTask( TimedOrbitTask, 10 ) + + self:Fuel() + RTB = true + end + else + end + end + + -- TODO: Check GROUP damage function. + local Damage = self.Controllable:GetLife() + local InitialLife = self.Controllable:GetLife0() + self:F( { Damage = Damage, InitialLife = InitialLife, DamageThreshold = self.PatrolDamageThreshold } ) + if ( Damage / InitialLife ) < self.PatrolDamageThreshold then + self:E( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) + self:Damaged() + RTB = true + self:SetStatusOff() + end + + -- Check if planes went RTB and are out of control. + -- We only check if planes are out of control, when they are in duty. + if self.Controllable:HasTask() == false then + if not self:Is( "Started" ) and + not self:Is( "Stopped" ) and + not self:Is( "Fuel" ) and + not self:Is( "Damaged" ) and + not self:Is( "Home" ) then + if self.IdleCount >= 2 then + if Damage ~= InitialLife then + self:Damaged() + else + self:E( self.Controllable:GetName() .. " control lost! " ) + self:LostControl() + end + else + self.IdleCount = self.IdleCount + 1 + end + end + else + self.IdleCount = 0 + end + + if RTB == true then + self:__RTB( 0.5 ) + end + + if not self:Is("Home") then + self:__Status( 10 ) + end + + end +end + + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.RTBRoute( AIGroup, Fsm ) + + AIGroup:F( { "AI_AIR.RTBRoute:", AIGroup:GetName() } ) + + if AIGroup:IsAlive() then + Fsm:__RTB( 0.5 ) + end + +end + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.RTBHold( AIGroup, Fsm ) + + AIGroup:F( { "AI_AIR.RTBHold:", AIGroup:GetName() } ) + if AIGroup:IsAlive() then + Fsm:__RTB( 0.5 ) + Fsm:Return() + local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) + AIGroup:SetTask( Task ) + end + +end + + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterRTB( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + + if AIGroup and AIGroup:IsAlive() then + + self:E( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) + + self:ClearTargetDistance() + AIGroup:ClearTasks() + + local EngageRoute = {} + + --- Calculate the target route point. + + local CurrentCoord = AIGroup:GetCoordinate() + local ToTargetCoord = self.HomeAirbase:GetCoordinate() + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + local ToAirbaseAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) + + local Distance = CurrentCoord:Get2DDistance( ToTargetCoord ) + + local ToAirbaseCoord = CurrentCoord:Translate( 5000, ToAirbaseAngle ) + if Distance < 5000 then + self:E( "RTB and near the airbase!" ) + self:Home() + return + end + --- Create a route point of type air. + local ToRTBRoutePoint = ToAirbaseCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = ToAirbaseAngle, ToTargetSpeed = ToTargetSpeed } ) + self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToRTBRoutePoint + EngageRoute[#EngageRoute+1] = ToRTBRoutePoint + + AIGroup:OptionROEHoldFire() + AIGroup:OptionROTEvadeFire() + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + AIGroup:WayPointInitialize( EngageRoute ) + + local Tasks = {} + Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_AIR.RTBRoute", self ) + EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) + + --- NOW ROUTE THE GROUP! + AIGroup:Route( EngageRoute, 0.5 ) + + end + +end + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterHome( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + self:E( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + end + +end + + + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterHold( AIGroup, From, Event, To, HoldTime ) + self:F( { AIGroup, From, Event, To } ) + + self:E( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + local OrbitTask = AIGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local TimedOrbitTask = AIGroup:TaskControlled( OrbitTask, AIGroup:TaskCondition( nil, nil, nil, nil, HoldTime , nil ) ) + + local RTBTask = AIGroup:TaskFunction( "AI_AIR.RTBHold", self ) + + local OrbitHoldTask = AIGroup:TaskOrbitCircle( 4000, self.PatrolMinSpeed ) + + --AIGroup:SetState( AIGroup, "AI_AIR", self ) + + AIGroup:SetTask( AIGroup:TaskCombo( { TimedOrbitTask, RTBTask, OrbitHoldTask } ), 1 ) + end + +end + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.Resume( AIGroup, Fsm ) + + AIGroup:I( { "AI_AIR.Resume:", AIGroup:GetName() } ) + if AIGroup:IsAlive() then + Fsm:__RTB( 0.5 ) + end + +end + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + self:E( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + local Tanker = GROUP:FindByName( self.TankerName ) + if Tanker:IsAlive() and Tanker:IsAirPlane() then + + local RefuelRoute = {} + + --- Calculate the target route point. + + local CurrentCoord = AIGroup:GetCoordinate() + local ToRefuelCoord = Tanker:GetCoordinate() + local ToRefuelSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + + --- Create a route point of type air. + local ToRefuelRoutePoint = ToRefuelCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToRefuelSpeed, + true + ) + + self:F( { ToRefuelSpeed = ToRefuelSpeed } ) + + RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint + RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint + + AIGroup:OptionROEHoldFire() + AIGroup:OptionROTEvadeFire() + + local Tasks = {} + Tasks[#Tasks+1] = AIGroup:TaskRefueling() + Tasks[#Tasks+1] = AIGroup:TaskFunction( self:GetClassName() .. ".Resume", self ) + RefuelRoute[#RefuelRoute].task = AIGroup:TaskCombo( Tasks ) + + AIGroup:Route( RefuelRoute, 0.5 ) + else + self:RTB() + end + end + +end + + + +--- @param #AI_AIR self +function AI_AIR:onafterDead() + self:SetStatusOff() +end + + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnCrash( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:E( self.Controllable:GetUnits() ) + if #self.Controllable:GetUnits() == 1 then + self:__Crash( 1, EventData ) + end + end +end + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnEjection( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__Eject( 1, EventData ) + end +end + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnPilotDead( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__PilotDead( 1, EventData ) + end +end From 4542c96eaee78cbf0e4fe382bbd5990e2d93ee44 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 9 Nov 2018 16:07:30 +0100 Subject: [PATCH 024/147] AB v0.2.1w --- Moose Development/Moose/Core/Radio.lua | 322 +++++++---- .../Moose/Functional/CarrierTrainer.lua | 519 +++++++++--------- Moose Development/Moose/Utilities/Utils.lua | 38 ++ .../Moose/Wrapper/Controllable.lua | 99 +++- 4 files changed, 607 insertions(+), 371 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 3810e18e3..89e6b9d95 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -9,12 +9,12 @@ -- -- The Radio contains 2 classes : RADIO and BEACON -- --- What are radio communications in DCS ? +-- What are radio communications in DCS? -- -- * Radio transmissions consist of **sound files** that are broadcasted on a specific **frequency** (e.g. 115MHz) and **modulation** (e.g. AM), -- * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmiter's antenna can be set, and the transmission can be **looped**. -- --- How to supply DCS my own Sound Files ? +-- How to supply DCS my own Sound Files? -- -- * Your sound files need to be encoded in **.ogg** or .wav, -- * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings, @@ -23,7 +23,7 @@ -- -- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or by any other @{Wrapper.Positionable#POSITIONABLE} -- --- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, +-- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, -- * If the transmitter is any other @{Wrapper.Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped. -- -- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft, @@ -33,7 +33,7 @@ -- -- === -- --- ### Author: Hugues "Grey_Echo" Bousquet +-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky -- -- @module Core.Radio -- @image Core_Radio.JPG @@ -66,24 +66,24 @@ -- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts -- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call -- --- What is this power thing ? +-- What is this power thing? -- -- * If your transmission is sent by a @{Wrapper.Positionable#POSITIONABLE} other than a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, you can set the power of the antenna, -- * Otherwise, DCS sets it automatically, depending on what's available on your Unit, --- * If the player gets **too far** from the transmiter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, +-- * If the player gets **too far** from the transmitter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, -- * This an automated DCS calculation you have no say on, --- * For reference, a standard VOR station has a 100W antenna, a standard AA TACAN has a 120W antenna, and civilian ATC's antenna usually range between 300 and 500W, +-- * For reference, a standard VOR station has a 100 W antenna, a standard AA TACAN has a 120 W antenna, and civilian ATC's antenna usually range between 300 and 500 W, -- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. -- -- @type RADIO --- @field Positionable#POSITIONABLE Positionable The transmiter --- @field #string FileName Name of the sound file --- @field #number Frequency Frequency of the transmission in Hz --- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM) --- @field #string Subtitle Subtitle of the transmission --- @field #number SubtitleDuration Duration of the Subtitle in seconds --- @field #number Power Power of the antenna is Watts --- @field #boolean Loop (default true) +-- @field Positionable#POSITIONABLE Positionable The transmiter. +-- @field #string FileName Name of the sound file played. +-- @field #number Frequency Frequency of the transmission in Hz. +-- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM). +-- @field #string Subtitle Subtitle of the transmission. +-- @field #number SubtitleDuration Duration of the Subtitle in seconds. +-- @field #number Power Power of the antenna is Watts. +-- @field #boolean Loop Transmission is repeated (default true). -- @extends Core.Base#BASE RADIO = { ClassName = "RADIO", @@ -96,12 +96,11 @@ RADIO = { Loop = true, } ---- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast --- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead +--- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast. +-- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead. -- @param #RADIO self -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #RADIO Radio --- @return #nil If Positionable is invalid +-- @return #RADIO The RADIO object or #nil if Positionable is invalid. function RADIO:New(Positionable) local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO @@ -113,11 +112,11 @@ function RADIO:New(Positionable) return self end - self:E({"The passed positionable is invalid, no RADIO created", Positionable}) + self:E({error="The passed positionable is invalid, no RADIO created!", positionable=Positionable}) return nil end ---- Check validity of the filename passed and sets RADIO.FileName +--- Set the file name for the radio transmission. -- @param #RADIO self -- @param #string FileName File name of the sound file (i.e. "Noise.ogg") -- @return #RADIO self @@ -125,49 +124,60 @@ function RADIO:SetFileName(FileName) self:F2(FileName) if type(FileName) == "string" then + if FileName:find(".ogg") or FileName:find(".wav") then if not FileName:find("l10n/DEFAULT/") then FileName = "l10n/DEFAULT/" .. FileName end + self.FileName = FileName return self end end - self:E({"File name invalid. Maybe something wrong with the extension ?", self.FileName}) + self:E({"File name invalid. Maybe something wrong with the extension?", FileName}) return self end ---- Check validity of the frequency passed and sets RADIO.Frequency +--- Set the frequency for the radio transmission. +-- If the transmitting positionable is a unit or group, this also set the command "SetFrequency" with the defined frequency and modulation. -- @param #RADIO self --- @param #number Frequency in MHz (Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz) +-- @param #number Frequency Frequency in MHz. Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz. -- @return #RADIO self function RADIO:SetFrequency(Frequency) self:F2(Frequency) + if type(Frequency) == "number" then + -- If frequency is in range if (Frequency >= 30 and Frequency < 88) or (Frequency >= 108 and Frequency < 152) or (Frequency >= 225 and Frequency < 400) then - self.Frequency = Frequency * 1000000 -- Conversion in Hz + + -- Convert frequency from MHz to Hz + self.Frequency = Frequency * 1000000 + + local commandSetFrequency={ + id = "SetFrequency", + params = { + frequency = self.Frequency, + modulation = self.Modulation, + } + } + -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ - id = "SetFrequency", - params = { - frequency = self.Frequency, - modulation = self.Modulation, - } - }) + self.Positionable:SetCommand(commandSetFrequency) end return self end end - self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", self.Frequency}) + + self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", Frequency}) return self end ---- Check validity of the frequency passed and sets RADIO.Modulation +--- Set AM or FM modulation of the radio transmitter. -- @param #RADIO self --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM +-- @param #number Modulation Modulation is either radio.modulation.AM or radio.modulation.FM. -- @return #RADIO self function RADIO:SetModulation(Modulation) self:F2(Modulation) @@ -183,23 +193,24 @@ end --- Check validity of the power passed and sets RADIO.Power -- @param #RADIO self --- @param #number Power in W +-- @param #number Power Power in W. -- @return #RADIO self function RADIO:SetPower(Power) self:F2(Power) + if type(Power) == "number" then self.Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that - return self + else + self:E({"Power is invalid. Power unchanged.", self.Power}) end - self:E({"Power is invalid. Power unchanged.", self.Power}) + return self end ---- Check validity of the loop passed and sets RADIO.Loop +--- Set message looping on or off. -- @param #RADIO self --- @param #boolean Loop +-- @param #boolean Loop If true, message is repeated indefinitely. -- @return #RADIO self --- @usage function RADIO:SetLoop(Loop) self:F2(Loop) if type(Loop) == "boolean" then @@ -246,10 +257,10 @@ end -- but it will work with a UNIT or a GROUP anyway. -- Only the #RADIO and the Filename are mandatory -- @param #RADIO self --- @param #string FileName --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #number Power in W +-- @param #string FileName Name of the sound file that will be transmitted. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation of frequency, which is either radio.modulation.AM or radio.modulation.FM. +-- @param #number Power Power in W. -- @return #RADIO self function RADIO:NewGenericTransmission(FileName, Frequency, Modulation, Power, Loop) self:F({FileName, Frequency, Modulation, Power}) @@ -365,22 +376,80 @@ end -- -- @type BEACON -- @field #string ClassName Name of the class "BEACON". --- @field Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities. -- @extends Core.Base#BASE BEACON = { ClassName = "BEACON", Positionable = nil, } ---- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.AATACAN} or @{#BEACON.Generic}. +--- Beacon types supported by DCS. +-- @type BEACON.Type +-- @field #number NULL +-- @field #number VOR +-- @field #number DME +-- @field #number VOR_DME +-- @field #number TACAN +-- @field #number VORTAC +-- @field #number RSBN +-- @field #number BROADCAST_STATION +-- @field #number HOMER +-- @field #number AIRPORT_HOMER +-- @field #number AIRPORT_HOMER_WITH_MARKER +-- @field #number ILS_FAR_HOMER +-- @field #number ILS_NEAR_HOMER +-- @field #number ILS_LOCALIZER +-- @field #number ILS_GLIDESLOPE +-- @field #number NAUTICAL_HOMER +-- @field #number ICLS +BEACON.Type={ + NULL = 0, + VOR = 1, + DME = 2, + VOR_DME = 3, + TACAN = 4, + VORTAC = 5, + RSBN = 32, + BROADCAST_STATION = 1024, + HOMER = 8, + AIRPORT_HOMER = 4104, + AIRPORT_HOMER_WITH_MARKER = 4136, + ILS_FAR_HOMER = 16408, + ILS_NEAR_HOMER = 16456, + ILS_LOCALIZER = 16640, + ILS_GLIDESLOPE = 16896, + NAUTICAL_HOMER = 32776, + ICLS = 131584, +} + +--- Beacon systems supported by DCS. +-- @type BEACON.System +-- @field #number PAR_10 +-- @field #number RSBN_5 +-- @field #number TACAN +-- @field #number TACAN_TANKER +-- @field #number ILS_LOCALIZER +-- @field #number ILS_GLIDESLOPE +-- @field #number BROADCAST_STATION +BEACON.System={ + PAR_10 = 1, + RSBN_5 = 2, + TACAN = 3, + TACAN_TANKER = 4, + ILS_LOCALIZER = 5, + ILS_GLIDESLOPE = 6, + BROADCAST_STATION = 7, +} + +--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc. -- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead. -- @param #BEACON self -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #BEACON Beacon or #nil if Positionable is invalid. +-- @return #BEACON Beacon object or #nil if the positionable is invalid. function BEACON:New(Positionable) -- Inherit BASE. - local self = BASE:Inherit(self, BASE:New()) --#BEACON + local self=BASE:Inherit(self, BASE:New()) --#BEACON -- Debug. self:F(Positionable) @@ -396,51 +465,13 @@ function BEACON:New(Positionable) end ---- Converts a TACAN Channel/Mode couple into a frequency in Hz --- @param #BEACON self --- @param #number TACANChannel --- @param #string TACANMode --- @return #number Frequecy --- @return #nil if parameters are invalid -function BEACON:_TACANToFrequency(TACANChannel, TACANMode) - self:F3({TACANChannel, TACANMode}) - - if type(TACANChannel) ~= "number" then - if TACANMode ~= "X" and TACANMode ~= "Y" then - return nil -- error in arguments - end - end - --- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. --- I have no idea what it does but it seems to work - local A = 1151 -- 'X', channel >= 64 - local B = 64 -- channel >= 64 - - if TACANChannel < 64 then - B = 1 - end - - if TACANMode == 'Y' then - A = 1025 - if TACANChannel < 64 then - A = 1088 - end - else -- 'X' - if TACANChannel < 64 then - A = 962 - end - end - - return (A + TACANChannel - B) * 1000000 -end - --- Activates a TACAN BEACON. -- @param #BEACON self --- @param #number TACANChannel TACAN channel, i.e. the "10" part in "10Y". --- @param #string TACANMode TACAN mode, i.e. the "Y" part in "10Y". Note that AA TACAN are only available on Y Channels. +-- @param #number Channel TACAN channel, i.e. the "10" part in "10Y". +-- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y". -- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon. -- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available. --- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. -- @return #BEACON self -- @usage -- -- Let's create a TACAN Beacon for a tanker @@ -448,11 +479,11 @@ end -- local myBeacon = myUnit:GetBeacon() -- Creates the beacon -- -- myBeacon:TACAN(20, "Y", "TEXACO", true) -- Activate the beacon -function BEACON:TACAN(TACANChannel, TACANMode, Message, Bearing, BeaconDuration) +function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) self:F({TACANChannel, Message, Bearing, BeaconDuration}) - -- Get frequency. - local Frequency = self:_TACANToFrequency(TACANChannel, TACANMode) + -- Get frequency. + local Frequency=UTILS.TACANToFrequency(Channel, Mode) -- Check. if not Frequency then @@ -462,57 +493,75 @@ function BEACON:TACAN(TACANChannel, TACANMode, Message, Bearing, BeaconDuration) if self.Positionable:IsAir() then --TODO: set TACANMode="Y" - self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft ! The BEACON is not emitting", self.Positionable}) + self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft! The BEACON is not emitting.", self.Positionable}) end - -- Using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the beacon shows its bearing or 14 (TACAN_AA_MODE_Y) if it does not. local System=14 if Bearing then System = 5 end - -- Beacon command https://wiki.hoggitworld.com/view/DCS_command_activateBeacon - local beaconcommand={ - id = "ActivateBeacon", - params = { - type = 4, --BEACON_TYPE_TACAN - system = System, - callsign = Message, - frequency = Frequency, - } - } + -- Beacon type. + local Type=BEACON.Type.TACAN + + -- Beacon system. + local System=BEACON.System.TACAN + + -- Check if unit is an aircraft and set system accordingly. + local AA=self.Positionable:IsAir() + if AA then + System=BEACON.System.TACAN_TANKER + end + + -- Attached unit. + local UnitID=self.Positionable:GetID() -- Debug - self:T2({"TACAN BEACON started!"}) - + self:T({"TACAN BEACON started!"}) + -- Start beacon. - self.Positionable:SetCommand(beaconcommand) + self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing) -- Stop sheduler - if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD - SCHEDULER:New(self, self.StopTACAN, {self}, BeaconDuration) + if Duration then -- Schedule the stop of the BEACON if asked by the MD + self.Positionable:DeactivateBeacon(Duration) end return self end ---- Stops the TACAN BEACON. +--- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system. -- @param #BEACON self +-- @param #number Channel ICLS channel. +-- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. -- @return #BEACON self -function BEACON:StopTACAN() - self:F() - if self.Positionable==nil then - self:E({"Start the beacon first before stoping it !"}) - else - local commandstop={id='DeactivateBeacon', params={}} - self.Positionable:SetCommand(commandstop) +function BEACON:ActivateICLS(Channel, Callsign, Duration) + self:F({Channel=Channel, Callsign=Callsign, Duration=Duration}) + + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug + self:T2({"ICLS BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign) + + -- Stop sheduler + if Duration then -- Schedule the stop of the BEACON if asked by the MD + self.Positionable:DeactivateBeacon(Duration) end + return self end + + + --- Activates a TACAN BEACON on an Aircraft. -- @param #BEACON self -- @param #number TACANChannel (the "10" part in "10Y"). Note that AA TACAN are only available on Y Channels @@ -678,4 +727,41 @@ function BEACON:StopRadioBeacon() return self end +--- Converts a TACAN Channel/Mode couple into a frequency in Hz +-- @param #BEACON self +-- @param #number TACANChannel +-- @param #string TACANMode +-- @return #number Frequecy +-- @return #nil if parameters are invalid +function BEACON:_TACANToFrequency(TACANChannel, TACANMode) + self:F3({TACANChannel, TACANMode}) + + if type(TACANChannel) ~= "number" then + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 4fed798e9..e4caa999f 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -1,4 +1,4 @@ ---- **Functional** - (R2.4) - Carrier CASE I Recovery Practice +--- **Functional** - (R2.5) - Manages aircraft operations on carriers. -- -- Practice carrier landings. -- @@ -16,11 +16,11 @@ -- -- ### Authors: **funkyfranky** (MOOSE class implementation and enhancements), **Bankler** (original idea and script) -- --- @module Functional.CarrierTrainer +-- @module Functional.Airboss -- @image MOOSE.JPG ---- CARRIERTRAINER class. --- @type CARRIERTRAINER +--- AIRBOSS class. +-- @type AIRBOSS -- @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. @@ -30,6 +30,7 @@ -- @field Core.Radio#BEACON beacon Carrier beacon for TACAN and ICLS. -- @field #number TACANchannel TACAN channel. -- @field #string TACANmode TACAN mode, i.e. "X" or "Y". +-- @field #number ICLSchannel ICLS channel. -- @field Core.Radio#RADIO LSOradio Radio for LSO calls. -- @field Core.Radio#RADIO Carrierradio Radio for carrier calls. -- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. @@ -37,14 +38,14 @@ -- @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 #AIRBOSS.Checkpoint Upwind Upwind checkpoint. +-- @field #AIRBOSS.Checkpoint BreakEarly Early break checkpoint. +-- @field #AIRBOSS.Checkpoint BreakLate Late brak checkpoint. +-- @field #AIRBOSS.Checkpoint Abeam Abeam checkpoint. +-- @field #AIRBOSS.Checkpoint Ninety At the ninety checkpoint. +-- @field #AIRBOSS.Checkpoint Wake Right behind the carrier. +-- @field #AIRBOSS.Checkpoint Groove In the groove checkpoint. +-- @field #AIRBOSS.Checkpoint Trap Landing checkpoint. -- @field #number rwyangle Angle of the runway wrt to carrier "nose". For the Stennis ~ -10 degrees. -- @field #number sterndist Distance in meters from carrier coordinate to the end of the deck. -- @field #number deckheight Height of the deck in meters. @@ -54,15 +55,15 @@ -- -- === -- --- ![Banner Image](..\Presentations\CARRIERTRAINER\CarrierTrainer_Main.png) +-- ![Banner Image](..\Presentations\AIRBOSS\CarrierTrainer_Main.png) -- -- # The Trainer Concept -- -- bla bla -- --- @field #CARRIERTRAINER -CARRIERTRAINER = { - ClassName = "CARRIERTRAINER", +-- @field #AIRBOSS +AIRBOSS = { + ClassName = "AIRBOSS", lid = nil, Debug = true, carrier = nil, @@ -71,7 +72,7 @@ CARRIERTRAINER = { beacon = nil, TACANchannel = nil, TACANmode = nil, - ICLS = nil, + ICLSchannel = nil, LSOradio = nil, LSOfreq = nil, Carrierradio = nil, @@ -97,21 +98,21 @@ CARRIERTRAINER = { } --- Aircraft types. --- @type CARRIERTRAINER.AircraftType +-- @type AIRBOSS.AircraftType -- @field #string AV8B AV-8B Night Harrier. -- @field #string HORNET F/A-18C Lot 20 Hornet. -CARRIERTRAINER.AircraftType={ +AIRBOSS.AircraftType={ AV8B="AV8BNA", HORNET="FA-18C_hornet", } --- Carrier types. --- @type CARRIERTRAINER.CarrierType +-- @type AIRBOSS.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={ +AIRBOSS.CarrierType={ STENNIS="Stennis", VINSON="Vinson", TARAWA="LHA_Tarawa", @@ -119,8 +120,8 @@ CARRIERTRAINER.CarrierType={ } --- Pattern steps. --- @type CARRIERTRAINER.PatternStep -CARRIERTRAINER.PatternStep={ +-- @type AIRBOSS.PatternStep +AIRBOSS.PatternStep={ UNREGISTERED="Unregistered", PATTERNENTRY="Pattern Entry", EARLYBREAK="Early Break", @@ -138,7 +139,7 @@ CARRIERTRAINER.PatternStep={ } --- LSO calls. --- @type CARRIERTRAINER.LSOcall +-- @type AIRBOSS.LSOcall -- @field Core.UserSound#USERSOUND RIGHTFORLINEUPL "Right for line up!" call (loud). -- @field Core.UserSound#USERSOUND RIGHTFORLINEUPS "Right for line up." call. -- @field #string RIGHTFORLINEUPT "Right for line up" text. @@ -161,7 +162,7 @@ CARRIERTRAINER.PatternStep={ -- @field #string BOLTERT "Bolter, bolter!" text. -- @field Core.UserSound#USERSOUND LONGGROOVE "You're long in the groove. Depart and re-enter." call. -- @field #string LONGGROOVET "You're long in the groove. Depart and re-enter." text. -CARRIERTRAINER.LSOcall={ +AIRBOSS.LSOcall={ RIGHTFORLINEUPL=USERSOUND:New("LSO - RightLineUp(L).ogg"), RIGHTFORLINEUPS=USERSOUND:New("LSO - RightLineUp(S).ogg"), RIGHTFORLINEUPT="Right for line up", @@ -187,18 +188,18 @@ CARRIERTRAINER.LSOcall={ } --- Difficulty level. --- @type CARRIERTRAINER.Difficulty +-- @type AIRBOSS.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={ +AIRBOSS.Difficulty={ EASY="Flight Student", NORMAL="Naval Aviator", HARD="TOPGUN Graduate", } --- Groove position. --- @type CARRIERTRAINER.GroovePos +-- @type AIRBOSS.GroovePos -- @field #string X0 Entering the groove. -- @field #string XX At the start, i.e. 3/4 from the run down. -- @field #string RB Roger ball. @@ -206,7 +207,7 @@ CARRIERTRAINER.Difficulty={ -- @field #string IC In close. -- @field #string AR At the ramp. -- @field #string IW In the wires. -CARRIERTRAINER.GroovePos={ +AIRBOSS.GroovePos={ X0="X0", XX="X", RB="RB", @@ -217,7 +218,7 @@ CARRIERTRAINER.GroovePos={ } --- Groove data. --- @type CARRIERTRAINER.GrooveData +-- @type AIRBOSS.GrooveData -- @field #number Step Current step. -- @field #number AoA Angle of Attack. -- @field #number Alt Altitude in meters. @@ -226,18 +227,18 @@ CARRIERTRAINER.GroovePos={ -- @field #number Roll Roll angle. --- LSO grade --- @type CARRIERTRAINER.LSOgrade +-- @type AIRBOSS.LSOgrade -- @field #string grade LSO grade, i.e. _OK_, OK, (OK), --, CUT -- @field #number points Points received. -- @field #string details Detailed flight analyis analysis. --- Player data table holding all important parameters of each player. --- @type CARRIERTRAINER.PlayerData +-- @type AIRBOSS.PlayerData -- @field Wrapper.Client#CLIENT client Client object of player. -- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field Wrapper.Group#GROUP group Aircraft group the player is in. -- @field #string callsign Callsign of player. -- @field #string difficulty Difficulty level. --- @field #number score Player score of the current pass. -- @field #number passes Number of passes. -- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. -- @field #table debrief Debrief analysis of the current step of this pass. @@ -250,10 +251,10 @@ CARRIERTRAINER.GroovePos={ -- @field #boolean patternwo If true, player was waved of during the pattern. -- @field #boolean lig If true, player was long in the groove. -- @field #number Tlso Last time the LSO gave an advice. --- @field #CARRIERTRAINER.GroovePos groove Data table at each position in the groove. Elemets are of type @{#CARRIERTRAINER.GrooveData}. +-- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. --- Checkpoint parameters triggering the next step in the pattern. --- @type CARRIERTRAINER.Checkpoint +-- @type AIRBOSS.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. @@ -271,11 +272,11 @@ CARRIERTRAINER.GroovePos={ --- Main radio menu. -- @field #table MenuF10 -CARRIERTRAINER.MenuF10={} +AIRBOSS.MenuF10={} --- Carrier trainer class version. -- @field #string version -CARRIERTRAINER.version="0.2.1" +AIRBOSS.version="0.2.1w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -299,14 +300,14 @@ CARRIERTRAINER.version="0.2.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Create new carrier trainer. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS 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 or nil if carrier unit does not exist. -function CARRIERTRAINER:New(carriername, alias) +-- @return #AIRBOSS self or nil if carrier unit does not exist. +function AIRBOSS:New(carriername, alias) -- Inherit everthing from FSM class. - local self = BASE:Inherit(self, FSM:New()) -- #CARRIERTRAINER + local self = BASE:Inherit(self, FSM:New()) -- #AIRBOSS -- Set carrier unit. self.carrier=UNIT:FindByName(carriername) @@ -325,7 +326,7 @@ function CARRIERTRAINER:New(carriername, alias) end -- Set some string id for output to DCS.log file. - self.lid=string.format("CARRIERTRAINER %s | ", carriername) + self.lid=string.format("AIRBOSS %s | ", carriername) -- Get carrier type. self.carriertype=self.carrier:GetTypeName() @@ -343,15 +344,15 @@ function CARRIERTRAINER:New(carriername, alias) self.Carrierradio=RADIO:New(self.carrier) self.LSOradio=RADIO:New(self.carrier) - if self.carriertype==CARRIERTRAINER.CarrierType.STENNIS then + if self.carriertype==AIRBOSS.CarrierType.STENNIS then self:_InitStennis() - elseif self.carriertype==CARRIERTRAINER.CarrierType.VINSON then + elseif self.carriertype==AIRBOSS.CarrierType.VINSON then -- TODO: Carl Vinson parameters. self:_InitStennis() - elseif self.carriertype==CARRIERTRAINER.CarrierType.TARAWA then + elseif self.carriertype==AIRBOSS.CarrierType.TARAWA then -- TODO: Tarawa parameters. self:_InitStennis() - elseif self.carriertype==CARRIERTRAINER.CarrierType.KUZNETSOV then + elseif self.carriertype==AIRBOSS.CarrierType.KUZNETSOV then -- TODO: Kusnetsov parameters - maybe... self:_InitStennis() else @@ -374,21 +375,21 @@ function CARRIERTRAINER:New(carriername, alias) --- Triggers the FSM event "Start" that starts the carrier trainer. Initializes parameters and starts event handlers. - -- @function [parent=#CARRIERTRAINER] Start - -- @param #CARRIERTRAINER self + -- @function [parent=#AIRBOSS] Start + -- @param #AIRBOSS 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 + -- @function [parent=#AIRBOSS] __Start + -- @param #AIRBOSS 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 + -- @function [parent=#AIRBOSS] Stop + -- @param #AIRBOSS 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 + -- @function [parent=#AIRBOSS] __Stop + -- @param #AIRBOSS self -- @param #number delay Delay in seconds. return self @@ -399,11 +400,11 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Set TACAN channel of carrier. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #number channel TACAN channel. -- @param #string mode TACAN mode, i.e. "X" or "Y". --- @return #CARRIERTRAINER self -function CARRIERTRAINER:SetTACAN(channel, mode) +-- @return #AIRBOSS self +function AIRBOSS:SetTACAN(channel, mode) self.TACANchannel=channel self.TACANmode=mode or "X" @@ -411,12 +412,23 @@ function CARRIERTRAINER:SetTACAN(channel, mode) return self end +--- Set ICLS channel of carrier. +-- @param #AIRBOSS self +-- @param #number channel ICLS channel. +-- @return #AIRBOSS self +function AIRBOSS:SetICLS(channel) + + self.ICLSchannel=channel + + return self +end + --- Set LSO radio frequency. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #number freq Frequency in MHz. --- @return #CARRIERTRAINER self -function CARRIERTRAINER:SetLSOradio(freq) +-- @return #AIRBOSS self +function AIRBOSS:SetLSOradio(freq) self.LSOfreq=freq @@ -424,10 +436,10 @@ function CARRIERTRAINER:SetLSOradio(freq) end --- Set carrier radio frequency. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #number freq Frequency in MHz. --- @return #CARRIERTRAINER self -function CARRIERTRAINER:SetCarrierradio(freq) +-- @return #AIRBOSS self +function AIRBOSS:SetCarrierradio(freq) self.Carrierfreq=freq @@ -439,18 +451,25 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function CARRIERTRAINER:onafterStart(From, Event, To) +function AIRBOSS: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)) + self:I(self.lid..string.format("Starting Carrier Training %s for carrier unit %s of type %s.", AIRBOSS.version, self.carrier:GetName(), self.carriertype)) -- Activate TACAN. - self.beacon:TACAN(self.TACANchannel, self.TACANmode, "STN", true, nil) + if self.TACANchannel~=nil and self.TACANmolde~=nil then + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, "STN", true) + end + -- Activate ICLS. + if self.ICLSchannel then + self.beacon:ActivateICLS(self.ICLSchannel, "STN") + end + -- Handle events. self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Land) @@ -461,11 +480,11 @@ function CARRIERTRAINER:onafterStart(From, Event, To) end --- On after Status event. Checks player status. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function CARRIERTRAINER:onafterStatus(From, Event, To) +function AIRBOSS:onafterStatus(From, Event, To) -- Check player status. self:_CheckPlayerStatus() @@ -475,22 +494,22 @@ function CARRIERTRAINER:onafterStatus(From, Event, To) end --- On after Stop event. Unhandle events and stop status updates. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function CARRIERTRAINER:onafterStop(From, Event, To) +function AIRBOSS:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Birth) self:UnHandleEvent(EVENTS.Land) end --- Carrier trainer event handler for event birth. --- @param #CARRIERTRAINER self -function CARRIERTRAINER:_CheckPlayerStatus() +-- @param #AIRBOSS self +function AIRBOSS:_CheckPlayerStatus() -- Loop over all players. for _playerName,_playerData in pairs(self.players) do - local playerData = _playerData --#CARRIERTRAINER.PlayerData + local playerData = _playerData --#AIRBOSS.PlayerData if playerData then @@ -584,9 +603,9 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Carrier trainer event handler for event birth. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData -function CARRIERTRAINER:OnEventBirth(EventData) +function AIRBOSS:OnEventBirth(EventData) self:F3({eventbirth = EventData}) local _unitName=EventData.IniUnitName @@ -610,7 +629,7 @@ function CARRIERTRAINER:OnEventBirth(EventData) local rightaircraft=false local aircraft=_unit:GetTypeName() - for _,actype in pairs(CARRIERTRAINER.AircraftType) do + for _,actype in pairs(AIRBOSS.AircraftType) do if actype==aircraft then rightaircraft=true end @@ -633,9 +652,9 @@ function CARRIERTRAINER:OnEventBirth(EventData) end --- Carrier trainer event handler for event land. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData -function CARRIERTRAINER:OnEventLand(EventData) +function AIRBOSS:OnEventLand(EventData) self:F3({eventland = EventData}) local _unitName=EventData.IniUnitName @@ -657,7 +676,7 @@ function CARRIERTRAINER:OnEventLand(EventData) MESSAGE:New(text, 5):ToAllIf(self.Debug) -- Player data. - local playerData=self.players[_playername] --#CARRIERTRAINER.PlayerData + local playerData=self.players[_playername] --#AIRBOSS.PlayerData -- Coordinate at landing event local coord=playerData.unit:GetCoordinate() @@ -691,13 +710,13 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Initialize player data. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #string unitname Name of the player unit. --- @return #CARRIERTRAINER.PlayerData Player data. -function CARRIERTRAINER:_InitPlayer(unitname) +-- @return #AIRBOSS.PlayerData Player data. +function AIRBOSS:_InitPlayer(unitname) -- Player data. - local playerData={} --#CARRIERTRAINER.PlayerData + local playerData={} --#AIRBOSS.PlayerData -- Player unit, client and callsign. playerData.unit = UNIT:FindByName(unitname) @@ -714,7 +733,7 @@ function CARRIERTRAINER:_InitPlayer(unitname) playerData.attitudemonitor=false -- Set difficulty level. - playerData.difficulty=playerData.difficulty or CARRIERTRAINER.Difficulty.NORMAL + playerData.difficulty=playerData.difficulty or AIRBOSS.Difficulty.NORMAL -- Player is in the big zone around the carrier. playerData.inbigzone=playerData.unit:IsInZone(self.giantZone) @@ -726,10 +745,10 @@ function CARRIERTRAINER:_InitPlayer(unitname) 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) +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return #AIRBOSS.PlayerData Initialized player data. +function AIRBOSS:_InitNewRound(playerData) self:I(self.lid..string.format("New round for player %s.", playerData.callsign)) playerData.step=0 playerData.groove={} @@ -745,9 +764,9 @@ function CARRIERTRAINER:_InitNewRound(playerData) end --- Initialize player data. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. -function CARRIERTRAINER:_NewRound(playerData) +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_NewRound(playerData) if playerData.unit:IsInZone(self.registerZone) then local text="Cleared for approach." @@ -761,16 +780,16 @@ function CARRIERTRAINER:_NewRound(playerData) end --- Start pattern when player enters the start zone. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Start(playerData) +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Start(playerData) -- Check if player is in start zone and about to enter the pattern. if playerData.unit:IsInZone(self.startZone) then -- Inform player. local hint = string.format("Entering the pattern.") - if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then + if playerData.difficulty==AIRBOSS.Difficulty.EASY then hint=hint.."Aim for 800 feet and 350 kts at the break entry." end @@ -784,9 +803,9 @@ function CARRIERTRAINER:_Start(playerData) end --- Upwind leg. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_Upwind(playerData) +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Upwind(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z = self:_GetDistances(playerData.unit) @@ -819,10 +838,10 @@ end --- Break. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. -- @param #string part Part of the break. -function CARRIERTRAINER:_Break(playerData, part) +function AIRBOSS:_Break(playerData, part) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z = self:_GetDistances(playerData.unit) @@ -868,9 +887,9 @@ function CARRIERTRAINER:_Break(playerData, part) end --- Long downwind leg check. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. -function CARRIERTRAINER:_CheckForLongDownwind(playerData) +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_CheckForLongDownwind(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z=self:_GetDistances(playerData.unit) @@ -889,10 +908,10 @@ function CARRIERTRAINER:_CheckForLongDownwind(playerData) if X 3 degrees. -- * Line up error > 3 degrees. -- * AoA<6.9 or AoA>9.3. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #number glideslopeError Glide slope error in degrees. -- @param #number lineupError Line up error in degrees. -- @param #number AoA Angle of attack of player aircraft. -- @return #boolean If true, player should wave off! -function CARRIERTRAINER:_CheckWaveOff(glideslopeError, lineupError, AoA) +function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA) local waveoff=false @@ -1304,10 +1323,10 @@ function CARRIERTRAINER:_CheckWaveOff(glideslopeError, lineupError, AoA) end --- Get name of the current pattern step. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #number step Step -- @return #string Name of the step -function CARRIERTRAINER:_GS(step) +function AIRBOSS:_GS(step) local gp if step==90 then gp="X0" -- Entering the groove. @@ -1328,10 +1347,10 @@ function CARRIERTRAINER:_GS(step) end --- Trapped? --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. -- @param Core.Point#COORDINATE pos Position of aircraft on landing event. -function CARRIERTRAINER:_Trapped(playerData, pos) +function AIRBOSS:_Trapped(playerData, pos) env.info("FF TRAPPED") @@ -1378,11 +1397,11 @@ function CARRIERTRAINER:_Trapped(playerData, pos) end --- Entering the Groove. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. -- @param #number glideslopeError Error in degrees. -- @param #number lineupError Error in degrees. -function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) +function AIRBOSS:_LSOcall(playerData, glideslopeError, lineupError) -- Player group. local player=playerData.unit:GetGroup() @@ -1391,16 +1410,16 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) local text="" if glideslopeError>1 then text="You're high!" - CARRIERTRAINER.LSOcall.HIGHL:ToGroup(player) + AIRBOSS.LSOcall.HIGHL:ToGroup(player) elseif glideslopeError>0.5 then text="You're a little high." - CARRIERTRAINER.LSOcall.HIGHS:ToGroup(player) + AIRBOSS.LSOcall.HIGHS:ToGroup(player) elseif glideslopeError<-1.0 then text="Power!" - CARRIERTRAINER.LSOcall.POWERL:ToGroup(player) + AIRBOSS.LSOcall.POWERL:ToGroup(player) elseif glideslopeError<-0.5 then text="You're a little low." - CARRIERTRAINER.LSOcall.POWERS:ToGroup(player) + AIRBOSS.LSOcall.POWERS:ToGroup(player) else text="Good altitude." end @@ -1417,16 +1436,16 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) -- Lineup left/right calls. if lineupError<-3 then text=text.."Come left!" - CARRIERTRAINER.LSOcall.COMELEFTL:ToGroup(player, delay) + AIRBOSS.LSOcall.COMELEFTL:ToGroup(player, delay) elseif lineupError<-1 then text=text.."Come left." - CARRIERTRAINER.LSOcall.COMELEFTS:ToGroup(player, delay) + AIRBOSS.LSOcall.COMELEFTS:ToGroup(player, delay) elseif lineupError>3 then text=text.."Right for lineup!" - CARRIERTRAINER.LSOcall.RIGHTFORLINEUPL:ToGroup(player, delay) + AIRBOSS.LSOcall.RIGHTFORLINEUPL:ToGroup(player, delay) elseif lineupError>1 then text=text.."Right for lineup." - CARRIERTRAINER.LSOcall.RIGHTFORLINEUPS:ToGroup(player, delay) + AIRBOSS.LSOcall.RIGHTFORLINEUPS:ToGroup(player, delay) else text=text.."Good lineup." end @@ -1460,10 +1479,10 @@ function CARRIERTRAINER:_LSOcall(playerData, glideslopeError, lineupError) end --- Get glide slope of aircraft. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. -- @return #number Glide slope angle in degrees measured from the -function CARRIERTRAINER:_Glideslope(playerData) +function AIRBOSS:_Glideslope(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi = self:_GetDistances(playerData.unit) @@ -1477,11 +1496,11 @@ function CARRIERTRAINER:_Glideslope(playerData) end --- Get line up of player wrt to carrier runway. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. -- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. -- @return #number Distance from carrier tail to player aircraft in meters. -function CARRIERTRAINER:_Lineup(playerData) +function AIRBOSS:_Lineup(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi = self:_GetDistances(playerData.unit) @@ -1507,18 +1526,18 @@ end --------- --- Append text to debrief text. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. +-- @param #AIRBOSS self +-- @param #AIRBOSS.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) +function AIRBOSS:_AddToSummary(playerData, step, item) table.insert(playerData.debrief, {step=step, hint=item}) end --- Show debriefing message. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. -function CARRIERTRAINER:_Debrief(playerData) +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Debrief(playerData) env.info("FF debrief") -- Debriefing text. @@ -1537,7 +1556,7 @@ function CARRIERTRAINER:_Debrief(playerData) -- LSO grade, points, and flight data analyis. local grade, points, analysis=self:_LSOgrade(playerData) - local mygrade={} --#CARRIERTRAINER.LSOgrade + local mygrade={} --#AIRBOSS.LSOgrade mygrade.grade=grade mygrade.points=points mygrade.details=analysis @@ -1563,10 +1582,10 @@ function CARRIERTRAINER:_Debrief(playerData) end --- Get relative heading of player wrt carrier. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Player unit. -- @return #number Relative heading in degrees. -function CARRIERTRAINER:_GetRelativeHeading(unit) +function AIRBOSS:_GetRelativeHeading(unit) local vC=self.carrier:GetOrientationX() local vP=unit:GetOrientationX() @@ -1579,10 +1598,10 @@ end --- Get name of the current pattern step. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #number step Step -- @return #string Name of the step -function CARRIERTRAINER:_StepName(step) +function AIRBOSS:_StepName(step) local name="unknown" if step==0 then @@ -1623,13 +1642,13 @@ function CARRIERTRAINER:_StepName(step) end --- Calculate distances between carrier and player unit. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS 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) +function AIRBOSS:_GetDistances(unit) -- Vector to carrier local a=self.carrier:GetVec3() @@ -1665,12 +1684,12 @@ function CARRIERTRAINER:_GetDistances(unit) end --- Check if a player is within the right area. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #number X X distance player to carrier. -- @param #number Z Z distance player to carrier. --- @param #CARRIERTRAINER.Checkpoint pos Position data limits. +-- @param #AIRBOSS.Checkpoint pos Position data limits. -- @return #boolean If true, approach should be aborted. -function CARRIERTRAINER:_CheckAbort(X, Z, pos) +function AIRBOSS:_CheckAbort(X, Z, pos) local abort=false if pos.Xmin and X=0 and X>=check.LimitXmin)) @@ -1966,12 +1985,12 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Grade approach. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. -- @return #string LSO grade, i.g. _OK_, OK, (OK), --, etc. -- @return #number Points. -- @return #string LSO analysis of flight path. -function CARRIERTRAINER:_LSOgrade(playerData) +function AIRBOSS:_LSOgrade(playerData) local function count(base, pattern) return select(2, string.gsub(base, pattern, "")) @@ -2048,11 +2067,11 @@ function CARRIERTRAINER:_LSOgrade(playerData) end --- Grade flight data. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.GrooveData fdata Flight data in the groove. +-- @param #AIRBOSS self +-- @param #AIRBOSS.GrooveData fdata Flight data in the groove. -- @return #string LSO grade or empty string if flight data table is nil. -- @return #number Number of deviations from perfect flight path. -function CARRIERTRAINER:_Flightdata2Text(fdata) +function AIRBOSS:_Flightdata2Text(fdata) local function little(text) return string.format("(%s)",text) @@ -2158,21 +2177,21 @@ function CARRIERTRAINER:_Flightdata2Text(fdata) end --- Evaluate player's altitude at checkpoint. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. -- @return #number Low score. -- @return #number Bad score. -function CARRIERTRAINER:_GetGoodBadScore(playerData) +function AIRBOSS:_GetGoodBadScore(playerData) local lowscore local badscore - if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then + if playerData.difficulty==AIRBOSS.Difficulty.EASY then lowscore=10 badscore=20 - elseif playerData.difficulty==CARRIERTRAINER.Difficulty.NORMAL then + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then lowscore=5 badscore=10 - elseif playerData.difficulty==CARRIERTRAINER.Difficulty.HARD then + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then lowscore=2.5 badscore=5 end @@ -2181,13 +2200,13 @@ function CARRIERTRAINER:_GetGoodBadScore(playerData) end --- Evaluate player's altitude at checkpoint. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. --- @param #CARRIERTRAINER.Checkpoint checkpoint Checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #AIRBOSS.Checkpoint checkpoint Checkpoint. -- @param #number altitude Player's current altitude in meters. -- @return #string Feedback text. -- @return #string Debriefing text. -function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint, altitude) +function AIRBOSS:_AltitudeCheck(playerData, checkpoint, altitude) -- Player altitude. local altitude=playerData.unit:GetAltitude() @@ -2212,11 +2231,11 @@ function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint, altitude) end -- Extend or decrease depending on skill. - if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then + if playerData.difficulty==AIRBOSS.Difficulty.EASY then hint=hint..string.format("Optimal altitude is %d ft.", UTILS.MetersToFeet(checkpoint.Altitude)) - elseif playerData.difficulty==CARRIERTRAINER.Difficulty.NORMAL then + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then --hint=hint.."\n" - elseif playerData.difficulty==CARRIERTRAINER.Difficulty.HARD then + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then hint="" end @@ -2227,13 +2246,13 @@ function CARRIERTRAINER:_AltitudeCheck(playerData, checkpoint, altitude) end --- Evaluate player's altitude at checkpoint. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data table. --- @param #CARRIERTRAINER.Checkpoint checkpoint Checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #AIRBOSS.Checkpoint checkpoint Checkpoint. -- @param #number distance Player's current distance to the boat in meters. -- @return #string Feedback message text. -- @return #string Debriefing text. -function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance) +function AIRBOSS:_DistanceCheck(playerData, checkpoint, distance) -- Get relative score. local lowscore, badscore = self:_GetGoodBadScore(playerData) @@ -2255,11 +2274,11 @@ function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance) end -- Extend or decrease depending on skill. - if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then + if playerData.difficulty==AIRBOSS.Difficulty.EASY then hint=hint..string.format(" Optimal distance is %d NM.", UTILS.MetersToNM(checkpoint.Distance)) - elseif playerData.difficulty==CARRIERTRAINER.Difficulty.NORMAL then + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then --hint=hint.."\n" - elseif playerData.difficulty==CARRIERTRAINER.Difficulty.HARD then + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then hint="" end @@ -2270,13 +2289,13 @@ function CARRIERTRAINER:_DistanceCheck(playerData, checkpoint, distance) end --- Score for correct AoA. --- @param #CARRIERTRAINER self --- @param #CARRIERTRAINER.PlayerData playerData Player data. --- @param #CARRIERTRAINER.Checkpoint checkpoint Checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #AIRBOSS.Checkpoint checkpoint Checkpoint. -- @param #number aoa Player's current Angle of attack. -- @return #string Feedback message text or easy and normal difficulty level or nil for hard. -- @return #string Debriefing text. -function CARRIERTRAINER:_AoACheck(playerData, checkpoint, aoa) +function AIRBOSS:_AoACheck(playerData, checkpoint, aoa) -- Get relative score. local lowscore, badscore = self:_GetGoodBadScore(playerData) @@ -2298,11 +2317,11 @@ function CARRIERTRAINER:_AoACheck(playerData, checkpoint, aoa) end -- Extend or decrease depending on skill. - if playerData.difficulty==CARRIERTRAINER.Difficulty.EASY then + if playerData.difficulty==AIRBOSS.Difficulty.EASY then hint=hint..string.format(" Optimal AoA is %.1f.", checkpoint.AoA) - elseif playerData.difficulty==CARRIERTRAINER.Difficulty.NORMAL then + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then --hint=hint.."\n" - elseif playerData.difficulty==CARRIERTRAINER.Difficulty.HARD then + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then hint="" end @@ -2314,14 +2333,14 @@ end --- Send message to playe client. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #string message The message to send. -- @param #number duration Display message duration. --- @param #CARRIERTRAINER.PlayerData playerData Player data. +-- @param #AIRBOSS.PlayerData playerData Player data. -- @param #boolean clear If true, clear screen from previous messages. -- @param #string sender The person who sends the message. Default is carrier alias. -- @param #number delay Delay in seconds, before the message is send. -function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData, clear, sender, delay) +function AIRBOSS:_SendMessageToPlayer(message, duration, playerData, clear, sender, delay) if message then delay=delay or 0 @@ -2343,11 +2362,11 @@ function CARRIERTRAINER:_SendMessageToPlayer(message, duration, playerData, clea 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 #AIRBOSS 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) +function AIRBOSS:_GetPlayerUnitAndName(_unitName) self:F2(_unitName) if _unitName ~= nil then @@ -2378,9 +2397,9 @@ end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Add menu commands for player. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #string _unitName Name of player unit. -function CARRIERTRAINER:_AddF10Commands(_unitName) +function AIRBOSS:_AddF10Commands(_unitName) self:F(_unitName) -- Get player unit and name. @@ -2401,15 +2420,15 @@ function CARRIERTRAINER:_AddF10Commands(_unitName) self.menuadded[_gid] = true -- Main F10 menu: F10/Carrier Trainer// - if CARRIERTRAINER.MenuF10[_gid] == nil then - CARRIERTRAINER.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "Carrier Trainer") + if AIRBOSS.MenuF10[_gid] == nil then + AIRBOSS.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "Carrier Trainer") end -- Player Data. local playerData=self.players[playername] -- F10/Carrier Trainer/ - local _trainPath = missionCommands.addSubMenuForGroup(_gid, self.alias, CARRIERTRAINER.MenuF10[_gid]) + local _trainPath = missionCommands.addSubMenuForGroup(_gid, self.alias, AIRBOSS.MenuF10[_gid]) -- F10/Carrier Trainer//Results local _statsPath = missionCommands.addSubMenuForGroup(_gid, "LSO Grades", _trainPath) @@ -2423,9 +2442,9 @@ function CARRIERTRAINER:_AddF10Commands(_unitName) --missionCommands.addCommandForGroup(_gid, "(Clear ALL Results)", _statsPath, self._ResetRangeStats, self, _unitName) -- F10/Carrier Trainer//Difficulty - missionCommands.addCommandForGroup(_gid, "Flight Student", _difficulPath, self._SetDifficulty, self, playername, CARRIERTRAINER.Difficulty.EASY) - missionCommands.addCommandForGroup(_gid, "Naval Aviator", _difficulPath, self._SetDifficulty, self, playername, CARRIERTRAINER.Difficulty.NORMAL) - missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _difficulPath, self._SetDifficulty, self, playername, CARRIERTRAINER.Difficulty.HARD) + missionCommands.addCommandForGroup(_gid, "Flight Student", _difficulPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) + missionCommands.addCommandForGroup(_gid, "Naval Aviator", _difficulPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) + missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _difficulPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) -- F10/Carrier Trainer// missionCommands.addCommandForGroup(_gid, "Carrier Info", _trainPath, self._DisplayCarrierInfo, self, _unitName) @@ -2445,9 +2464,9 @@ function CARRIERTRAINER:_AddF10Commands(_unitName) end --- Display top 10 player scores. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function CARRIERTRAINER:_DisplayPlayerGrades(_unitName) +function AIRBOSS:_DisplayPlayerGrades(_unitName) self:F(_unitName) -- Get player unit and name. @@ -2455,7 +2474,7 @@ function CARRIERTRAINER:_DisplayPlayerGrades(_unitName) -- Check if we have a unit which is a player. if _unit and _playername then - local playerData=self.players[_playername] --#CARRIERTRAINTER.PlayerData + local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then @@ -2464,7 +2483,7 @@ function CARRIERTRAINER:_DisplayPlayerGrades(_unitName) local p=0 for i,_grade in pairs(playerData.grades) do - local grade=_grade --#CARRIERTRAINER.LSOgrade + local grade=_grade --#AIRBOSS.LSOgrade text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, grade.points, grade.details) p=p+grade.points @@ -2491,9 +2510,9 @@ end --- Display top 10 player scores. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function CARRIERTRAINER:_DisplayScoreBoard(_unitName) +function AIRBOSS:_DisplayScoreBoard(_unitName) self:F(_unitName) -- Get player unit and name. @@ -2506,7 +2525,7 @@ function CARRIERTRAINER:_DisplayScoreBoard(_unitName) local _playerResults={} -- Player data of requestor. - local playerData=self.players[_playername] --#CARRIERTRAINER.PlayerData + local playerData=self.players[_playername] --#AIRBOSS.PlayerData -- Message text. local text = string.format("Greenie Board:") @@ -2543,12 +2562,12 @@ end --- Turn player's aircraft attitude display on or off. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #string playername Player name. -function CARRIERTRAINER:_AttitudeMonitor(playername) +function AIRBOSS:_AttitudeMonitor(playername) self:E({playername=playername}) - local playerData=self.players[playername] --#CARRIERTRAINER.PlayerData + local playerData=self.players[playername] --#AIRBOSS.PlayerData if playerData then playerData.attitudemonitor=not playerData.attitudemonitor @@ -2556,13 +2575,13 @@ function CARRIERTRAINER:_AttitudeMonitor(playername) end --- Set difficulty level. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #string playername Player name. --- @param #CARRIERTRAINER.Difficulty difficulty Difficulty level. -function CARRIERTRAINER:_SetDifficulty(playername, difficulty) +-- @param #AIRBOSS.Difficulty difficulty Difficulty level. +function AIRBOSS:_SetDifficulty(playername, difficulty) self:E({difficulty=difficulty, playername=playername}) - local playerData=self.players[playername] --#CARRIERTRAINER.PlayerData + local playerData=self.players[playername] --#AIRBOSS.PlayerData if playerData then playerData.difficulty=difficulty @@ -2574,9 +2593,9 @@ function CARRIERTRAINER:_SetDifficulty(playername, difficulty) end --- Report information about carrier. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. -function CARRIERTRAINER:_DisplayCarrierInfo(_unitname) +function AIRBOSS:_DisplayCarrierInfo(_unitname) self:E(_unitname) -- Get player unit and player name. @@ -2586,7 +2605,7 @@ function CARRIERTRAINER:_DisplayCarrierInfo(_unitname) if unit and playername then -- Player data. - local playerData=self.players[playername] --#CARRIERTRAINER.PlayerData + local playerData=self.players[playername] --#AIRBOSS.PlayerData if playerData then @@ -2606,7 +2625,7 @@ function CARRIERTRAINER:_DisplayCarrierInfo(_unitname) if self.TACAN~=nil then tacan=tostring(self.TACAN) end - if self.ICLS~=nil then + if self.ICLSchannel~=nil then icls=tostring(self.ICLS) end @@ -2628,9 +2647,9 @@ end --- Report weather conditions at the carrier location. Temperature, QFE pressure and wind data. --- @param #CARRIERTRAINER self +-- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. -function CARRIERTRAINER:_DisplayCarrierWeather(_unitname) +function AIRBOSS:_DisplayCarrierWeather(_unitname) self:E(_unitname) -- Get player unit and player name. diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 42dce2b58..2c7068a83 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -680,3 +680,41 @@ function UTILS.VecCross(a, b) return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} end +--- Converts a TACAN Channel/Mode couple into a frequency in Hz. +-- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". +-- @param #string TACANMode The TACAN mode, i.e. the "X" in "10X". +-- @return #number Frequency in Hz or #nil if parameters are invalid. +function UTILS.TACANToFrequency(TACANChannel, TACANMode) + + if type(TACANChannel) ~= "number" then + return nil -- error in arguments + end + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end + + + diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index d349aa88d..30a1d73dd 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -372,7 +372,7 @@ end --- Clearing the Task Queue and Setting the Task on the queue from the controllable. -- @param #CONTROLLABLE self --- @param #DCS.Task DCSTask DCS Task array. +-- @param DCS#Task DCSTask DCS Task array. -- @param #number WaitTime Time in seconds, before the task is set. -- @return Wrapper.Controllable#CONTROLLABLE self function CONTROLLABLE:SetTask( DCSTask, WaitTime ) @@ -640,9 +640,102 @@ function CONTROLLABLE:StartUncontrolled(delay) return self end +--- Give the CONTROLLABLE the command to activate a beacon. See See https://wiki.hoggitworld.com/view/DCS_command_activateBeacon +-- For specific beacons like TACAN use the more convenient @{#BEACON} class. +-- @param #CONTROLLABLE self +-- @param Core.Radio#BEACON.Type Type Beacon type (VOR, DME, TACAN, RSBN, ILS etc). +-- @param Core.Radio#BEACON.System System Beacon system (VOR, DME, TACAN, RSBN, ILS etc). +-- @param #number Frequency Frequency in Hz the beacon is running on. Use @{#UTILS.TACANToFrequency} to generate a frequency for TACAN beacons. +-- @param #number UnitID The ID of the unit the beacon is attached to. Usefull if more units are in one group. +-- @param #number Channel Channel the beacon is using. For, e.g. TACAN beacons. +-- @param #string ModeChannel The TACAN mode of the beacon, i.e. "X" or "Y". +-- @param #boolean AA If true, create and Air-Air beacon. IF nil, automatically set if CONTROLLABLE is an air unit. +-- @param #string Callsign Morse code identification callsign. +-- @param #boolean Bearing If true, beacon provides bearing information (if supported). +-- @param #number Delay (Optional) Delay in seconds before the beacon is activated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing, Delay) + + AA=AA or self:IsAir() + UnitID=UnitID or self:GetID() + + -- Command + local CommandActivateBeacon= { + id = "ActivateBeacon", + params = { + ["type"] = Type, + ["system"] = System, + ["frequency"] = Frequency, + ["unitId"] = UnitID, + ["channel"] = Channel, + ["modeChannel"] = ModeChannel, + ["AA"] = AA, + ["callsign"] = Callsign, + ["bearing"] = Bearing, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self, Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing}, Delay) + else + self:SetCommand(CommandActivateBeacon) + end + + return self +end + +--- Activate ICLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! +-- @param #CONTROLLABLE self +-- @param #number Channel ICLS channel. +-- @param #number UnitID The ID of the unit the ICLS system is attached to. Useful if more units are in one group. +-- @param #string Callsign Morse code identification callsign. +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateICLS(Channel, UnitID, Callsign, Delay) + self:F() + + -- Command to activate ICLS system. + local CommandActivateICLS= { + id = "ActivateICLS", + params= { + ["type"] = BEACON.Type.ICLS, + ["channel"] = Channel, + ["unitId"] = UnitID, + ["callsign"] = Callsign, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateICLS, {self}, Delay) + else + self:SetCommand(CommandActivateICLS) + end + + return self +end + + +--- Deactivate the active beacon of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the beacon is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateBeacon(Delay) + self:F() + + -- Command to deactivate + local CommandDeactivateBeacon={id='DeactivateBeacon', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self}, Delay) + else + self:SetCommand(CommandDeactivateBeacon) + end + + return self +end + + -- TASKS FOR AIR CONTROLLABLES - - --- (AIR) Attack a Controllable. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. From f76b0dc7d363581621a107244c8cd1f4f34052c3 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 9 Nov 2018 16:47:06 +0100 Subject: [PATCH 025/147] Select correct template to engage is working now also. --- Moose Development/Moose/AI/AI_A2G_Dispatcher.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 33b855f9c..dca7c2086 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -2207,7 +2207,7 @@ do -- AI_A2G_DISPATCHER --- -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:CountDefenders( AttackerDetection, DefenderCount ) + function AI_A2G_DISPATCHER:CountDefenders( AttackerDetection, DefenderCount, DefenderTaskType ) local Friendlies = nil @@ -2225,8 +2225,8 @@ do -- AI_A2G_DISPATCHER -- Now we need to check if the AIGroup has a Task. local DefenderTask = self:GetDefenderTask( FriendlyGroup ) if DefenderTask then - -- The Task should be SEAD - if true then -- TODO: fix this to the correct DefenderTaskType + -- The Task should be of the same type. + if DefenderTaskType == DefenderTask.Type then -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet if DefenderTask.Target == nil then if DefenderTask.Fsm:Is( "Returning" ) From 900a55614b30e85d3293fe6d93d39215a1bd0ed4 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 9 Nov 2018 17:00:40 +0100 Subject: [PATCH 026/147] Change in defense reactivity and distance calculations. --- Moose Development/Moose/AI/AI_A2G_Dispatcher.lua | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index dca7c2086..0d378551d 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -397,6 +397,9 @@ do -- AI_A2G_DISPATCHER self.DefenderPatrolIndex = 0 + self:SetDefenseDistance() + self:SetDefenseReactivityMedium() + self:__Start( 5 ) return self @@ -519,19 +522,22 @@ do -- AI_A2G_DISPATCHER --- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:SetDefenseReactivityLow() self.DefenseReactivity = 0.05 - self.DefenseDistance = 20000 end --- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:SetDefenseReactivityMedium() self.DefenseReactivity = 0.15 - self.DefenseDistance = 20000 end --- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:SetDefenseReactivityHigh() self.DefenseReactivity = 0.5 - self.DefenseDistance = 20000 + end + + --- @param #AI_A2G_DISPATCHER self + -- @param #number DefensiveDistance The distance in meters from where the evaluation of defense reactivity will be calculated. + function AI_A2G_DISPATCHER:SetDefenseDistance( DefensiveDistance ) + self.DefenseDistance = DefensiveDistance or 60000 end end From cf4be99093accaef2b0f1b77542c09ca40fef95b Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 10 Nov 2018 00:57:14 +0100 Subject: [PATCH 027/147] AB v0.2.2 --- Moose Development/Moose/Core/Point.lua | 21 ++-- Moose Development/Moose/Core/Radio.lua | 14 +-- .../Moose/Functional/CarrierTrainer.lua | 119 ++++++++++++++++-- .../Moose/Wrapper/Controllable.lua | 30 ++++- 4 files changed, 145 insertions(+), 39 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 25efa066e..8a6e0ffda 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -342,18 +342,18 @@ do -- COORDINATE return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z end - --- Returns if the 2 coordinates are at the same 2D position. + --- Scan/find objects (units, statics, scenery) within a certain radius around the coordinate using the world.searchObjects() DCS API function. -- @param #COORDINATE self -- @param #number radius (Optional) Scan radius in meters. Default 100 m. -- @param #boolean scanunits (Optional) If true scan for units. Default true. -- @param #boolean scanstatics (Optional) If true scan for static objects. Default true. -- @param #boolean scanscenery (Optional) If true scan for scenery objects. Default false. - -- @return True if units were found. - -- @return True if statics were found. - -- @return True if scenery objects were found. - -- @return Unit objects found. - -- @return Static objects found. - -- @return Scenery objects found. + -- @return #boolean True if units were found. + -- @return #boolean True if statics were found. + -- @return #boolean True if scenery objects were found. + -- @return #table Table of MOOSE @[#Wrapper.Unit#UNIT} objects found. + -- @return #table Table of DCS static objects found. + -- @return #table Table of DCS scenery objects found. function COORDINATE:ScanObjects(radius, scanunits, scanstatics, scanscenery) self:F(string.format("Scanning in radius %.1f m.", radius)) @@ -405,18 +405,17 @@ do -- COORDINATE local ObjectCategory = ZoneObject:getCategory() -- Check for unit or static objects - --if (ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive()) then - if (ObjectCategory == Object.Category.UNIT and ZoneObject:isExist()) then + if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() then table.insert(Units, UNIT:Find(ZoneObject)) gotunits=true - elseif (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then table.insert(Statics, ZoneObject) gotstatics=true - elseif ObjectCategory == Object.Category.SCENERY then + elseif ObjectCategory==Object.Category.SCENERY then table.insert(Scenery, ZoneObject) gotscenery=true diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 89e6b9d95..90bfa23ef 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -480,7 +480,7 @@ end -- -- myBeacon:TACAN(20, "Y", "TEXACO", true) -- Activate the beacon function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) - self:F({TACANChannel, Message, Bearing, BeaconDuration}) + self:I({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) -- Get frequency. local Frequency=UTILS.TACANToFrequency(Channel, Mode) @@ -496,12 +496,6 @@ function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft! The BEACON is not emitting.", self.Positionable}) end - -- Using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the beacon shows its bearing or 14 (TACAN_AA_MODE_Y) if it does not. - local System=14 - if Bearing then - System = 5 - end - -- Beacon type. local Type=BEACON.Type.TACAN @@ -517,14 +511,14 @@ function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) -- Attached unit. local UnitID=self.Positionable:GetID() - -- Debug + -- Debug. self:T({"TACAN BEACON started!"}) -- Start beacon. self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing) - -- Stop sheduler - if Duration then -- Schedule the stop of the BEACON if asked by the MD + -- Stop sheduler. + if Duration then self.Positionable:DeactivateBeacon(Duration) end diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index e4caa999f..ef68dfc38 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -14,7 +14,7 @@ -- -- === -- --- ### Authors: **funkyfranky** (MOOSE class implementation and enhancements), **Bankler** (original idea and script) +-- ### Authors: **funkyfranky**, **Bankler** (Carrier trainer idea and script) -- -- @module Functional.Airboss -- @image MOOSE.JPG @@ -34,7 +34,7 @@ -- @field Core.Radio#RADIO LSOradio Radio for LSO calls. -- @field Core.Radio#RADIO Carrierradio Radio for carrier calls. -- @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 carrierZone 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. @@ -49,6 +49,8 @@ -- @field #number rwyangle Angle of the runway wrt to carrier "nose". For the Stennis ~ -10 degrees. -- @field #number sterndist Distance in meters from carrier coordinate to the end of the deck. -- @field #number deckheight Height of the deck in meters. +-- @field #table Qmarshal Queue of marshalling aircraft groups. +-- @field #table Qpattern Queue of aircraft groups in the landing pattern. -- @extends Core.Fsm#FSM --- Practice Carrier Landings @@ -79,7 +81,7 @@ AIRBOSS = { Carrierfreq = nil, registerZone = nil, startZone = nil, - giantZone = nil, + carrierZone = nil, players = {}, menuadded = {}, Upwind = {}, @@ -90,7 +92,7 @@ AIRBOSS = { Wake = {}, Groove = {}, Trap = {}, - rwyangle = -10, + rwyangle = -9, sterndist =-100, deckheight = 22, Qpattern = {}, @@ -276,7 +278,7 @@ AIRBOSS.MenuF10={} --- Carrier trainer class version. -- @field #string version -AIRBOSS.version="0.2.1w" +AIRBOSS.version="0.2.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -312,11 +314,14 @@ function AIRBOSS:New(carriername, alias) -- Set carrier unit. self.carrier=UNIT:FindByName(carriername) + -- Carrier zones. if self.carrier then - -- Carrier zones. - 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}) + -- Zone 5 km astern and 100 m starboard of the carrier with radius of 2.5 km. + self.registerZone = ZONE_UNIT:New("registerZone", self.carrier, 2.5*1000, {dx = -5000, dy = 100, relative_to_unit=true}) + -- Zone 2 km astern and 100 m starboard of the carrier with a radius of 1 km. + self.startZone = ZONE_UNIT:New("startZone", self.carrier, 1.0*1000, {dx = -2000, dy = 100, relative_to_unit=true}) + -- Zone around the carrier with a radius of 30 km. + self.carrierZone = ZONE_UNIT:New("carrierZone", self.carrier, 10.0*1000) else -- Carrier unit does not exist error. 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) @@ -461,7 +466,7 @@ function AIRBOSS:onafterStart(From, Event, To) self:I(self.lid..string.format("Starting Carrier Training %s for carrier unit %s of type %s.", AIRBOSS.version, self.carrier:GetName(), self.carriertype)) -- Activate TACAN. - if self.TACANchannel~=nil and self.TACANmolde~=nil then + if self.TACANchannel~=nil and self.TACANmode~=nil then self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, "STN", true) end @@ -486,6 +491,9 @@ end -- @param #string To To state. function AIRBOSS:onafterStatus(From, Event, To) + -- Scan carrier zone for new aircraft. + self:_ScanCarrierZone() + -- Check player status. self:_CheckPlayerStatus() @@ -503,7 +511,92 @@ function AIRBOSS:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Land) end ---- Carrier trainer event handler for event birth. +--- Check if new aircraft arrived +-- @param #AIRBOSS self +function AIRBOSS:_ScanCarrierZone() + + env.info("FF Scanning Carrier Zone") + + -- Carrier position. + local coord=self.carrier:GetCoordinate() + + -- Scan units in carrier zone. + local _,_,_,unitsin =coord:ScanObjects(10*1000, true, false, false) + --local _,_,_,unitsout=coord:ScanObjects(15*1000, true, false, false) + + for _,_unit in pairs(unitsin) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit:IsAir() then + + local group=unit:GetGroup() + local unitname=unit:GetName() + local groupname=group:GetName() + + local text=string.format("In carrier zone: unit=%s group=%s", unitname, groupname) + --env.info(text) + + if self.Qmarshal[groupname]==nil then + + env.info("FF marshal group="..groupname) + self:_Marshal(group) + + end + + end + end + +--[[ + for _,_unitin in pairs(unitsin) do + local unitin=_unitin --Wrapper.Unit#UNIT + if unit:IsAir()() then + local text=string.format("Aircraft in carrier zone = ", unit:GetName()) + env.info(text) + end + end +]] + +end + +--- Orbit at a specified position at a specified alititude with a specified speed. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Group +function AIRBOSS:_Marshal(group) + + local groupname=group:GetName() + + local Coord=self.carrier:GetCoordinate() + local Altitude=UTILS.FeetToMeters(2000) + local Speed=UTILS.KnotsToMps(272) + + local DCSTask={} + DCSTask.id="ControlledTask" + DCSTask.params={} + DCSTask.params.task=group:TaskOrbit(Coord, Altitude, Speed) + DCSTask.params.stopCondition={userFlag=groupname, userFlagValue=1} + + -- Set waypoint landing on Carrier. + local wp={} + wp[1]=self.carrier:GetCoordinate():SetAltitude(Altitude):WaypointAirTurningPoint(nil, Speed, {DCSTask}, string.format("Marshal @ %d ft %d knots", Altitude, Speed)) + wp[2]=self.carrier:GetCoordinate():WaypointAirLanding(Speed, AIRBASE:FindByName(self.carrier:GetName()), nil, "Landing") + group:WayPointInitialize(wp) + group:Route(wp, 0) + + self.Qmarshal[groupname]=group +end + + + +--- Check if new aircraft group arrived. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +function AIRBOSS:_AddGroupMarshall(group) + local groupname=group:GetName() + self.Qmarshal[groupname]=group + +end + +--- Check current player status. -- @param #AIRBOSS self function AIRBOSS:_CheckPlayerStatus() @@ -523,7 +616,7 @@ function AIRBOSS:_CheckPlayerStatus() self:_DetailedPlayerStatus(playerData) end - if unit:IsInZone(self.giantZone) then + if unit:IsInZone(self.carrierZone) then -- Check if player was previously not inside the zone. if playerData.inbigzone==false then @@ -736,7 +829,7 @@ function AIRBOSS:_InitPlayer(unitname) playerData.difficulty=playerData.difficulty or AIRBOSS.Difficulty.NORMAL -- Player is in the big zone around the carrier. - playerData.inbigzone=playerData.unit:IsInZone(self.giantZone) + playerData.inbigzone=playerData.unit:IsInZone(self.carrierZone) -- Init stuff for this round. playerData=self:_InitNewRound(playerData) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 30a1d73dd..91bc437a4 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -550,9 +550,9 @@ end ---- Executes a command action +--- Executes a command action for the CONTROLLABLE. -- @param #CONTROLLABLE self --- @param DCS#Command DCSCommand +-- @param DCS#Command DCSCommand The command to be executed. -- @return #CONTROLLABLE self function CONTROLLABLE:SetCommand( DCSCommand ) self:F2( DCSCommand ) @@ -640,8 +640,9 @@ function CONTROLLABLE:StartUncontrolled(delay) return self end ---- Give the CONTROLLABLE the command to activate a beacon. See See https://wiki.hoggitworld.com/view/DCS_command_activateBeacon +--- Give the CONTROLLABLE the command to activate a beacon. See [DCS_command_activateBeacon](https://wiki.hoggitworld.com/view/DCS_command_activateBeacon) on Hoggit. -- For specific beacons like TACAN use the more convenient @{#BEACON} class. +-- Note that a controllable can only have one beacon activated at a time with the execption of ICLS. -- @param #CONTROLLABLE self -- @param Core.Radio#BEACON.Type Type Beacon type (VOR, DME, TACAN, RSBN, ILS etc). -- @param Core.Radio#BEACON.System System Beacon system (VOR, DME, TACAN, RSBN, ILS etc). @@ -649,9 +650,9 @@ end -- @param #number UnitID The ID of the unit the beacon is attached to. Usefull if more units are in one group. -- @param #number Channel Channel the beacon is using. For, e.g. TACAN beacons. -- @param #string ModeChannel The TACAN mode of the beacon, i.e. "X" or "Y". --- @param #boolean AA If true, create and Air-Air beacon. IF nil, automatically set if CONTROLLABLE is an air unit. +-- @param #boolean AA If true, create and Air-Air beacon. IF nil, automatically set if CONTROLLABLE depending on whether unit is and aircraft or not. -- @param #string Callsign Morse code identification callsign. --- @param #boolean Bearing If true, beacon provides bearing information (if supported). +-- @param #boolean Bearing If true, beacon provides bearing information - if supported by the unit the beacon is attached to. -- @param #number Delay (Optional) Delay in seconds before the beacon is activated. -- @return #CONTROLLABLE self function CONTROLLABLE:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing, Delay) @@ -734,6 +735,25 @@ function CONTROLLABLE:CommandDeactivateBeacon(Delay) return self end +--- Deactivate the ICLS of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateICLS(Delay) + self:F() + + -- Command to deactivate + local CommandDeactivateICLS={id='DeactivateICLS', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandDeactivateICLS, {self}, Delay) + else + self:SetCommand(CommandDeactivateICLS) + end + + return self +end + -- TASKS FOR AIR CONTROLLABLES --- (AIR) Attack a Controllable. From 3ca712a2234b7ba320e5b4c42e3791e16a6b6b65 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 10 Nov 2018 12:42:16 +0100 Subject: [PATCH 028/147] Optimized the overhead problem in case of engage from patrol. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 55 +++++++++++++++++-- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 0d378551d..114505266 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -1480,6 +1480,44 @@ do -- AI_A2G_DISPATCHER end + --- Gets the overhead of planes as part of the defense system, in comparison with the attackers. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- local SquadronOverhead = A2GDispatcher:GetSquadronOverhead( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:GetSquadronOverhead( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Overhead or self.DefenderDefault.Overhead + end + + --- Sets the default grouping of new airplanes spawned. -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. -- @param #AI_A2G_DISPATCHER self @@ -2193,12 +2231,10 @@ do -- AI_A2G_DISPATCHER local DefenderSquadronName = DefenderTask.SquadronName if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then - local Squadron = self:GetSquadron( DefenderSquadronName ) - local SquadronOverhead = Squadron.Overhead or self.DefenderDefault.Overhead local DefenderSize = Defender:GetInitialSize() if DefenderSize then - DefenderCount = DefenderCount + DefenderSize / SquadronOverhead + DefenderCount = DefenderCount + DefenderSize self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) else DefenderCount = 0 @@ -2430,12 +2466,19 @@ do -- AI_A2G_DISPATCHER for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do + local SquadronName = self:GetDefenderTask( DefenderGroup ).SquadronName + local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) + local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) Fsm:__Engage( 1, AttackerSet ) -- Engage on the TargetSetUnit self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) - DefenderCount = DefenderCount + DefenderGroup:GetSize() + DefendersMissing = DefendersMissing - DefenderGroup:GetSize() / SquadronOverhead + + if DefendersMissing <= 0 then + break + end end self:F( { DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) @@ -2599,7 +2642,7 @@ do -- AI_A2G_DISPATCHER local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT local AttackerCount = AttackerSet:Count() - local IsSEAD = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group? + local IsSEAD = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group?µ if ( IsSEAD > 0 ) then @@ -2641,7 +2684,7 @@ do -- AI_A2G_DISPATCHER -- First, count the active defenders, engaging the DetectedItem. local DefenderCount = self:CountDefendersEngaged( DetectedItem ) - + local DefendersMissing = AttackerCount - DefenderCount self:F( { AttackerCount = AttackerCount, DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) From 74123d8ff340883fe1008fe00a4bfa3f807465f4 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 10 Nov 2018 13:21:46 +0100 Subject: [PATCH 029/147] Fixed overhead bug in dispatchiing from ground. --- Moose Development/Moose/AI/AI_A2G_Dispatcher.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 114505266..209a89965 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -2231,10 +2231,13 @@ do -- AI_A2G_DISPATCHER local DefenderSquadronName = DefenderTask.SquadronName if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then + + local Squadron = self:GetSquadronFromDefender( Defender ) + local SquadronOverhead = self:GetSquadronOverhead( Squadron.SquadronName ) local DefenderSize = Defender:GetInitialSize() if DefenderSize then - DefenderCount = DefenderCount + DefenderSize + DefenderCount = DefenderCount + DefenderSize / SquadronOverhead self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) else DefenderCount = 0 @@ -2474,7 +2477,7 @@ do -- AI_A2G_DISPATCHER self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) - DefendersMissing = DefendersMissing - DefenderGroup:GetSize() / SquadronOverhead + DefendersMissing = DefendersMissing - DefenderGroup:GetSize() if DefendersMissing <= 0 then break From caa3bcb02b1dfe461eaef2567b437dc9c03a36c3 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 10 Nov 2018 16:30:00 +0100 Subject: [PATCH 030/147] Introduction documentation of A2G dispatching. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 200 +++++++++++------- 1 file changed, 126 insertions(+), 74 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 209a89965..185cf4fd1 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -1,26 +1,24 @@ ---- **AI** - Manages the process of an automatic A2G defense system based on a detection network, coordinating SEAD, BAI and CAP operations. +--- **AI** - Manages the process of an automatic A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. -- -- === -- -- Features: -- -- * Setup quickly an A2G defense system for a coalition. --- * Setup (SEAD) Suppression at defined zones to enhance your A2G defenses. --- * Setup (CAS) Controlled Air Support for nearby enemy ground units. --- * Setup (BAI) Battleground Air Interdiction for remote enemy ground units and targets. --- * Define and use an detection network setup by recce. +-- * Setup multiple defense zones to defend specif points in your battlefield. +-- * Setup (SEAD) suppression of air defenses to enhance the control of enemy airspace. +-- * Setup (CAS) Controlled Air Support to attack approach enemy ground units. +-- * Setup (BAI) Battleground Air Interdiction to attack detected remote enemy ground units and targets. +-- * Define and use a detection network setup by recce. -- * Define defense squadrons at airbases, farps and carriers. -- * Enable airbases for A2G defenses. --- * Add different planes and helicopter types to different squadrons. +-- * Add different planes and helicopter templates to different squadrons. -- * Assign squadrons to execute a specific engagement type depending on threat level of the detected ground enemy unit composition. --- * Add multiple squadrons to different airbases. +-- * Add multiple squadrons to different airbases, farps or carriers. -- * Define different ranges to engage upon. --- * Define zones of defense. Detected targets nearby these zones are more critical than other detected targets. -- * Establish an automatic in air refuel process for planes using refuel tankers. -- * Setup default settings for all squadrons and A2G defenses. -- * Setup specific settings for specific squadrons. --- * Quickly setup an A2G defense system using @{#AI_A2G_SEADCAPBAI}. --- * Setup a more advanced defense system using @{#AI_A2G_DISPATCHER}. -- -- === -- @@ -38,92 +36,117 @@ -- -- # QUICK START GUIDE -- --- There are basically two classes available to model an A2G defense system. +-- The following class is available to model an A2G defense system. -- --- AI\_A2G\_DISPATCHER is the main A2G defense class that models the A2G defense system. --- AI\_A2G\_GCICAP derives or inherits from AI\_A2G\_DISPATCHER and is a more **noob** user friendly class, but is less flexible. +-- AI_A2G_DISPATCHER is the main A2G defense class that models the A2G defense system. -- --- Before you start using the AI\_A2G\_DISPATCHER or AI\_A2G\_GCICAP ask youself the following questions. +-- Before you start using the AI_A2G_DISPATCHER, ask youself the following questions. +-- -- +-- ## 1. Which coalition am I modeling an A2G defense system for? blue or red? -- --- ## 0. Do I need AI\_A2G\_DISPATCHER or do I need AI\_A2G\_GCICAP? --- --- AI\_A2G\_GCICAP, automates a lot of the below questions using the mission editor and requires minimal lua scripting. --- But the AI\_A2G\_GCICAP provides less flexibility and a lot of options are defaulted. --- With AI\_A2G\_DISPATCHER you can setup a much more **fine grained** A2G defense mechanism, but some more (easy) lua scripting is required. --- --- ## 1. Which Coalition am I modeling an A2G defense system for? blue or red? --- --- One AI\_A2G\_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. --- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI\_A2G\_DISPATCHER **objects**, --- each governing their defense system. +-- One AI_A2G_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. +-- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI_A2G_DISPATCHER **objects**, +-- each governing their defense system for one coalition. -- -- --- ## 2. Which type of EWR will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). +-- ## 2. Which type of detection will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). -- --- The MOOSE framework leverages the @{Detection} classes to perform the EWR detection. --- Several types of @{Detection} classes exist, and the most common characteristics of these classes is that they: +-- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units and reporting them to the head quarters. +-- Several types of @{Functional.Detection} classes exist, and the most common characteristics of these classes is that they: -- --- * Perform detections from multiple FACs as one co-operating entity. --- * Communicate with a Head Quarters, which consolidates each detection. +-- * Perform detections from multiple recce as one co-operating entity. +-- * Communicate with a @{Tasking.CommandCenter}, which consolidates each detection. -- * Groups detections based on a method (per area, per type or per unit). -- * Communicates detections. -- --- ## 3. Which EWR units will be used as part of the detection system? Only Ground or also Airborne? -- --- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. --- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). --- Additionally, ANY other radar capable unit can be part of the EWR network! Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. --- The position of these units is very important as they need to provide enough coverage --- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. +-- ## 3. Which recce units can be used as part of the detection system? Only Ground or also Airborne? -- --- ## 4. Is a border required? +-- Depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets. +-- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range. +-- Recce are very useful to acquire the position of enemy ground targets when spread out over the battlefield at strategic positions. +-- Ground units also have varying detectors, and especially the ground units which have laser guiding missiles can be extremely effective at +-- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. +-- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then +-- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! -- --- Is this a cold car or a hot war situation? In case of a cold war situation, a border can be set that will only trigger defenses --- if the border is crossed by enemy units. +-- Beside ground level units to use for reconnaissance, air units are also very effective. The are capable of patrolling at great speed +-- covering a large terrain. However, airborne recce can be vulnerable to air to ground attacks, and you need air superiority to make then +-- effective. Also the instruments available at the air units play a big role in the effectiveness of the reconnaissance. +-- Air units which have ground detection capabilities will be much more effective than air units with only visual detection capabilities. +-- For the red coalition, the Mi-28N and for the blue side, the reaper are such effective reconnaissance airborne units. -- --- ## 5. What maximum range needs to be checked to allow defenses to engage any attacker? -- --- A good functioning defense will have a "maximum range" evaluated to the enemy when CAP will be engaged or GCI will be spawned. +-- ## 4. How do defenses decide to engage on approaching enemy units? -- --- ## 6. Which Airbases, Carrier Ships, Farps will take part in the defense system for the Coalition? +-- The A2G dispacher needs you to setup defense coordinates, which are specific coordinates that are strategic positions in the battle field +-- to be defended. Any ground based enemy approaching to such a defense point, will be engaged for defense by A2G defense units. +-- The A2G dispatcher provides parameters to setup the defensiveness, meaning, when actually A2G units will engage with the approaching enemy. +-- For this, a probability distribution model has been created, which models an increased probability that a defense will engage an attacker, +-- depending on the distance of the attacker to the defense coordinate. There are 3 levels of defense reactivity setup, which are Low, Medium and High. +-- Defenses will start to consider defensive action when an enemy ground unit is within 60km from a defense point, by default. +-- But you can change this maximum distance using on of the available methods. The close the attacker is to the defense point, the +-- higher the probability will be that a defense action will be launched! -- --- Carefully plan which airbases will take part in the coalition. Color each airbase in the color of the coalition. -- --- ## 7. Which Squadrons will I create and which name will I give each Squadron? +-- ## 5. Are defense coordinates and defense reactivity the only parameters? -- --- The defense system works with Squadrons. Each Squadron must be given a unique name, that forms the **key** to the defense system. +-- No, depending on the target type, and the threat level of the target, the probability of defense will be higher. +-- In other words, when a SAM-10 radar emitter is detected, its probabilty for defense will be much higher than when a BMP-1 vehicle is +-- detected, even when both are at the same distance from a defense coordinate. +-- This will ensure optimal defenses, SEAD tasks will be much more quicker launched agains radar emitters, to ensure air superiority. +-- Approaching main battle tanks will be much faster defended upon, than a group of approaching trucks. +-- +-- +-- ## 6. Which Squadrons will I create and which name will I give each Squadron? +-- +-- The A2G defense system works with **Squadrons**. Each Squadron must be given a unique name, that forms the **key** to the squadron. -- Several options and activities can be set per Squadron. -- --- ## 8. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? +-- There are mainly 3 types of defenses: SEAD, CAS and BAI. +-- +-- Suppression of Air Defenses (SEAD) are effective agains radar emitters. Close Air Support (CAS) is launched when the enemy is close near friendly units. +-- Battleground Air Interdiction (BAI) tasks are launched when there are no friendlies around. +-- +-- Depending on the defense type, different payloads will be needed. See further points on squadron definition. +-- +-- ## 7. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? -- -- Squadrons are placed as the "home base" on an airfield, carrier or farp. -- Carefully plan where each Squadron will be located as part of the defense system. +-- Any airbase, farp or carrier can act as the launching platform for A2G defenses. +-- Carefully plan which airbases will take part in the coalition. Color each airbase in the color of the coalition. -- --- ## 9. Which plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron? -- --- Per Squadron, one or multiple plane models can be allocated as **Templates**. +-- ## 8. Which helicopter or plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron? +-- +-- Per Squadron, one or multiple helicopter or plane models can be allocated as **Templates**. -- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**. -- The A2G defense system will select from the given templates a random template to spawn a new plane (group). +-- +-- A squadron will perform specific task types (SEAD, CAS or BAI). So, squadrons will require specific templates for the +-- task types it will perform. A squadron executing SEAD defenses, will require a payload with long range anti-radar seeking missiles. -- --- ## 10. Which payloads, skills and skins will these plane models have? +-- +-- ## 9. Which payloads, skills and skins will these plane models have? -- -- Per Squadron, even if you have one plane model, you can still allocate multiple templates of one plane model, -- each having different payloads, skills and skins. -- The A2G defense system will select from the given templates a random template to spawn a new plane (group). -- --- ## 11. For each Squadron, which will perform CAP? -- --- Per Squadron, evaluate which Squadrons will perform CAP. --- Not all Squadrons need to perform CAP. +-- ## 10. How to squadrons engage in a defensive action? -- --- ## 12. For each Squadron doing CAP, in which ZONE(s) will the CAP be performed? +-- There are two ways how squadrons engage and execute your A2G defenses. +-- Squadrons can start the defense directly from the airbase, farp or carrier. When a squadron launches a defensive group, that group +-- will start directly from the airbase. The other way is to launch early on in the mission a patrolling mechanism. +-- Squadrons will launch air units to patrol in specific zone(s), so that when ground enemy targets are detected, that the airborne +-- A2G defenses can come immediately into action. -- --- Per CAP, evaluate **where** the CAP will be performed, in other words, define the **zone**. --- Near the border or a bit further away? -- --- ## 13. For each Squadron doing CAP, which zone types will I create? +-- ## 11. For each Squadron doing a patrol, which zone types will I create? -- --- Per CAP zone, evaluate whether you want: +-- Per zone, evaluate whether you want: -- -- * simple trigger zones -- * polygon zones @@ -131,19 +154,36 @@ -- -- Depending on the type of zone selected, a different @{Zone} object needs to be created from a ZONE_ class. -- --- ## 14. For each Squadron doing CAP, what are the time intervals and CAP amounts to be performed? -- --- For each CAP: +-- ## 12. Are moving defense coordinates possible? -- --- * **How many** CAP you want to have airborne at the same time? --- * **How frequent** you want the defense mechanism to check whether to start a new CAP? +-- Yes, different COORDINATE types are possible to be used. +-- The COORDINATE_UNIT will help you to specify a defense coodinate that is attached to a moving unit. -- --- ## 15. For each Squadron, which will perform GCI? -- --- For each Squadron, evaluate which Squadrons will perform GCI? --- Not all Squadrons need to perform GCI. +-- ## 13. How much defense coordinates do I need to create? -- --- ## 16. For each Squadron, which takeoff method will I use? +-- It depends, but the idea is to define only the necessary defense points that drive your mission. +-- If you define too much defense points, the performance of your mission may decrease. Per defense point defined, +-- all the possible enemies are evaluated. Note that each defense coordinate has a reach depending on the size of the defense radius. +-- The default defense radius is about 60km, and depending on the defense reactivity, defenses will be launched when the enemy is at +-- close or greater distance from the defense coordinate. +-- +-- +-- ## 14. For each Squadron doing patrols, what are the time intervals and patrol amounts to be performed? +-- +-- For each patrol: +-- +-- * **How many** patrol you want to have airborne at the same time? +-- * **How frequent** you want the defense mechanism to check whether to start a new patrol? +-- +-- other considerations: +-- +-- * **How far** is the patrol area from the engagement "hot zone". You want to ensure that the enemy is reached on time! +-- * **How safe** is the patrol area taking into account air superiority. Is it well defended, are there nearby A2A bases? +-- +-- +-- ## 15. For each Squadron, which takeoff method will I use? -- -- For each Squadron, evaluate which takeoff method will be used: -- @@ -153,8 +193,11 @@ -- * From a parking spot with cold engines -- -- **The default takeoff method is staight in the air.** +-- This takeoff method is the most useful if you want to avoid airplane clutter at airbases! +-- But it is the least realistic one! -- --- ## 17. For each Squadron, which landing method will I use? +-- +-- ## 16. For each Squadron, which landing method will I use? -- -- For each Squadron, evaluate which landing method will be used: -- @@ -163,27 +206,36 @@ -- * Despawn after engine shutdown after landing -- -- **The default landing method is despawn when near the airbase when returning.** +-- This landing method is the most useful if you want to avoid airplane clutter at airbases! +-- But it is the least realistic one! -- --- ## 18. For each Squadron, which overhead will I use? -- --- For each Squadron, depending on the airplane type (modern, old) and payload, which overhead is required to provide any defense? --- In other words, if **X** attacker airplanes are detected, how many **Y** defense airplanes need to be spawned per squadron? +-- ## 19. For each Squadron, which **defense overhead** will I use? +-- +-- For each Squadron, depending on the helicopter or airplane type (modern, old) and payload, which overhead is required to provide any defense? +-- +-- In other words, if **X** enemy ground units are detected, how many **Y** defense helicpters or airplanes need to engage (per squadron)? -- The **Y** is dependent on the type of airplane (era), payload, fuel levels, skills etc. --- The overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected. +-- But the most important factor is the payload, which is the amount of A2G weapons the defense can carry to attack the enemy ground units. +-- For example, a Ka-50 can carry 16 vikrs, that means, that it potentially can destroy at least 8 ground units without a reload of ammunication. +-- That means, that one defender can destroy more enemy ground units. +-- Thus, the overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected. +-- +-- **The default overhead is 1. A smaller value than 1, like 0.25 will decrease the overhead to a 1 / 4 ratio, meaning, +-- one defender for each 4 detected ground enemy units. ** -- --- **The default overhead is 1. A value greater than 1, like 1.5 will increase the overhead with 50%, a value smaller than 1, like 0.5 will decrease the overhead with 50%.** -- -- ## 19. For each Squadron, which grouping will I use? -- --- When multiple targets are detected, how will defense airplanes be grouped when multiple defense airplanes are spawned for multiple attackers? +-- When multiple targets are detected, how will defenses be grouped when multiple defense air units are spawned for multiple enemy ground units? -- Per one, two, three, four? -- -- **The default grouping is 1. That means, that each spawned defender will act individually.** +-- But you can specify a number between 1 and 4, so that the defenders will act as a group. -- -- === -- --- ### Authors: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons). --- ### Authors: **Stonehouse**, **SNAFU** in terms of the advice, documentation, and the original GCICAP script. +-- ### Author: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons). -- -- @module AI.AI_A2G_Dispatcher -- @image AI_Air_To_Air_Dispatching.JPG From c4984a7576a4c0529b3fcbd5742bdd2d528d11c1 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 11 Nov 2018 07:22:22 +0100 Subject: [PATCH 031/147] Doc update --- Moose Development/Moose/AI/AI_A2G_Dispatcher.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 185cf4fd1..f92803859 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -41,7 +41,8 @@ -- AI_A2G_DISPATCHER is the main A2G defense class that models the A2G defense system. -- -- Before you start using the AI_A2G_DISPATCHER, ask youself the following questions. --- -- +-- +-- -- ## 1. Which coalition am I modeling an A2G defense system for? blue or red? -- -- One AI_A2G_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. From ed60c5dca4a031561369f76c507d20e27a143643 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 11 Nov 2018 07:25:47 +0100 Subject: [PATCH 032/147] Images --- Moose Development/Moose/AI/AI_A2G_Dispatcher.lua | 2 +- Moose Development/Moose/AI/AI_A2G_Engage.lua | 4 ++-- Moose Development/Moose/AI/AI_A2G_Patrol.lua | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index f92803859..520ef9228 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -239,7 +239,7 @@ -- ### Author: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons). -- -- @module AI.AI_A2G_Dispatcher --- @image AI_Air_To_Air_Dispatching.JPG +-- @image AI_Air_To_Ground_Dispatching.JPG diff --git a/Moose Development/Moose/AI/AI_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua index 26e993420..2cc6845b1 100644 --- a/Moose Development/Moose/AI/AI_A2G_Engage.lua +++ b/Moose Development/Moose/AI/AI_A2G_Engage.lua @@ -8,8 +8,8 @@ -- -- === -- --- @module AI.AI_A2G_ENGAGE --- @image AI_Ground_Control_Engage.JPG +-- @module AI.AI_A2G_Engage +-- @image AI_Air_To_Ground_Engage.JPG diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua index a21929d03..bf4a97dba 100644 --- a/Moose Development/Moose/AI/AI_A2G_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2G_Patrol.lua @@ -7,7 +7,7 @@ -- === -- -- @module AI.AI_A2G_Patrol --- @image AI_Combat_Air_Patrol.JPG +-- @image AI_Air_To_Ground_Patrol.JPG --- @type AI_A2G_PATROL -- @extends AI.AI_A2A_Patrol#AI_A2A_PATROL From 252950af73c74b0eca5165e90a186ff1a7cf35cb Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 11 Nov 2018 08:43:20 +0100 Subject: [PATCH 033/147] Fixed overhead (again), and optimized defense radius logic. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 108 +++++++++++++++--- 1 file changed, 90 insertions(+), 18 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 520ef9228..6a5d40729 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -450,7 +450,6 @@ do -- AI_A2G_DISPATCHER self.DefenderPatrolIndex = 0 - self:SetDefenseDistance() self:SetDefenseReactivityMedium() self:__Start( 5 ) @@ -586,12 +585,6 @@ do -- AI_A2G_DISPATCHER function AI_A2G_DISPATCHER:SetDefenseReactivityHigh() self.DefenseReactivity = 0.5 end - - --- @param #AI_A2G_DISPATCHER self - -- @param #number DefensiveDistance The distance in meters from where the evaluation of defense reactivity will be calculated. - function AI_A2G_DISPATCHER:SetDefenseDistance( DefensiveDistance ) - self.DefenseDistance = DefensiveDistance or 60000 - end end @@ -677,7 +670,9 @@ do -- AI_A2G_DISPATCHER -- function AI_A2G_DISPATCHER:SetDefenseRadius( DefenseRadius ) - self.DefenseRadius = DefenseRadius or 100000 + self.DefenseRadius = DefenseRadius or 100000 + + self.Detection:SetAcceptRange( self.DefenseRadius ) return self end @@ -1123,20 +1118,27 @@ do -- AI_A2G_DISPATCHER end - --- Set the squadron Patrol parameters. + --- Set the squadron patrol parameters for a specific task type. + -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. + -- + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for SEAD tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for CAS tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for BAI tasks. + -- -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. -- @param #number Probability Is not in use, you can skip this parameter. + -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". -- @return #AI_A2G_DISPATCHER -- @usage -- -- -- Patrol Squadron execution. -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- A2GDispatcher:SetSquadronPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Mineralnye", 2, 30, 60, 1, "SEAD" ) -- function AI_A2G_DISPATCHER:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, DefenseTaskType ) @@ -1166,6 +1168,72 @@ do -- AI_A2G_DISPATCHER end end + + --- Set the squadron Patrol parameters for SEAD tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronSeadPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "SEAD" ) + + end + + + --- Set the squadron Patrol parameters for CAS tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronCasPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronCasPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "CAS" ) + + end + + + --- Set the squadron Patrol parameters for BAI tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronBaiPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronBaiPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "BAI" ) + + end + --- -- @param #AI_A2G_DISPATCHER self @@ -2530,7 +2598,7 @@ do -- AI_A2G_DISPATCHER self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) - DefendersMissing = DefendersMissing - DefenderGroup:GetSize() + DefendersMissing = DefendersMissing - DefenderGroup:GetSize() / SquadronOverhead if DefendersMissing <= 0 then break @@ -2859,16 +2927,20 @@ do -- AI_A2G_DISPATCHER local DefenseCoordinate = nil for DefenseCoordinateName, EvaluateCoordinate in pairs( self.DefenseCoordinates ) do - + local EvaluateDistance = AttackerCoordinate:Get2DDistance( EvaluateCoordinate ) - local DistanceProbability = ( self.DefenseDistance / EvaluateDistance * self.DefenseReactivity ) - local DefenseProbability = math.random() - self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) + if EvaluateDistance <= self.DefenseRadius then - if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then - DefenseCoordinate = EvaluateCoordinate - break + local DistanceProbability = ( self.DefenseRadius / EvaluateDistance * self.DefenseReactivity ) + local DefenseProbability = math.random() + + self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) + + if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then + DefenseCoordinate = EvaluateCoordinate + break + end end end From ea3899f4694a21455ba966f4144d1b73d4129a46 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 11 Nov 2018 21:50:41 +0100 Subject: [PATCH 034/147] More documentation for A2G dispatcher. --- .../Moose/AI/AI_A2A_Dispatcher.lua | 2 +- .../Moose/AI/AI_A2G_Dispatcher.lua | 202 +++++++++++++++++- 2 files changed, 201 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index a884daf88..9b9370bb3 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -274,7 +274,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher_Red = AI_A2A_DISPATCHER:New( EWR_Red ) -- A2ADispatcher_Blue = AI_A2A_DISPATCHER:New( EWR_Blue ) -- - -- ### 2. Define the detected **target grouping radius**: + -- ### 1.2. Define the detected **target grouping radius**: -- -- The target grouping radius is a property of the Detection object, that was passed to the AI\_A2A\_DISPATCHER object, but can be changed. -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 6a5d40729..26eda3fc5 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -1,4 +1,4 @@ ---- **AI** - Manages the process of an automatic A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. +--- **AI** - Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. -- -- === -- @@ -249,10 +249,208 @@ do -- AI_A2G_DISPATCHER -- @type AI_A2G_DISPATCHER -- @extends Tasking.DetectionManager#DETECTION_MANAGER - --- Create an automatic air defence system for a coalition. + --- Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. -- -- === -- + -- When your mission is in the need to take control of the AI to automate and setup a process of air to ground defenses, this is the module you need. + -- The defense system work through the definition of defense coordinates, which are points in your friendly area within the battle field, that your mission need to have defended. + -- Multiple defense coordinates can be setup. Defense coordinates can be strategic or tactical positions or references to strategic units or scenery. + -- The A2G dispatcher will evaluate every x seconds the tactical situation around each defense coordinate. When a defense coordinate + -- is under threat, it will communicate through the command center that defensive actions need to be taken and will launch groups of air units for defense. + -- The level of threat to the defense coordinate varyies upon the strength and types of the enemy units, the distance to the defense point, and the defensiveness parameters. + -- Defensive actions are taken through probability, but the closer and the more threat the enemy poses to the defense coordinate, the faster it will be attacked by friendly A2G units. + -- + -- Please study carefully the underlying explanations how to setup and use this module, as it has many features. + -- It also requires a little study to ensure that you get a good understanding of the defense mechanisms, to ensure a strong + -- defense for your missions. + -- + -- === + -- + -- # USAGE GUIDE + -- + -- ## 1. AI\_A2G\_DISPATCHER constructor: + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_DISPATCHER-ME_1.JPG) + -- + -- + -- The @{#AI_A2G_DISPATCHER.New}() method creates a new AI_A2G_DISPATCHER instance. + -- + -- ### 1.1. Define the **reconnaissance network**: + -- + -- As part of the AI_A2G_DISPATCHER :New() constructor, a reconnaissance network must be given as the first parameter. + -- A reconnaissance network is provide through an instance of a @{Functional.Detection} network. + -- The most effective reconnaissance for the A2G dispatcher would be to use the @{Functional.Detection#DETECTION_AREAS} object. + -- + -- An reconnaissance network, is used to detect enemy ground targets, potentially group them into areas, and to understand the position, level of threat of the enemy. + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia5.JPG) + -- + -- As explained in the introduction, depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets. + -- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range. + -- Recce are very useful to acquire the position of enemy ground targets when spread out over the battlefield at strategic positions. + -- Ground units also have varying detectors, and especially the ground units which have laser guiding missiles can be extremely effective at + -- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. + -- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then + -- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! + -- + -- Beside ground level units to use for reconnaissance, air units are also very effective. The are capable of patrolling at great speed + -- covering a large terrain. However, airborne recce can be vulnerable to air to ground attacks, and you need air superiority to make then + -- effective. Also the instruments available at the air units play a big role in the effectiveness of the reconnaissance. + -- Air units which have ground detection capabilities will be much more effective than air units with only visual detection capabilities. + -- For the red coalition, the Mi-28N and for the blue side, the reaper are such effective reconnaissance airborne units. + -- + -- Reconnaissance networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection} instance that is given as the first parameter to the A2G dispatcher. + -- By defining in a **smart way the names or name prefixes of the reconnaissance groups**, these groups will be **automatically added or removed** to or from the reconnaissance network, + -- when these groups are spawned in or destroyed during the ongoing battle. + -- By spawning in dynamically additional recce, you can ensure that there is sufficient reconnaissance coverage so the defense mechanism is continuously + -- alerted of new enemy ground targets. + -- + -- The following example defens a new reconnaissance network using a @{Functional.Detection#DETECTION_AREAS} object. + -- + -- -- Define a SET_GROUP object that builds a collection of groups that define the recce network. + -- -- Here we build the network with all the groups that have a name starting with CCCP Recce. + -- DetectionSetGroup = SET_GROUP:New() -- Defene a set of group objects, caled DetectionSetGroup. + -- + -- DetectionSetGroup:FilterPrefixes( { "CCCP Recce" } ) -- The DetectionSetGroup will search for groups that start with the name "CCCP Recce". + -- + -- -- This command will start the dynamic filtering, so when groups spawn in or are destroyed, + -- -- which have a group name starting with "CCCP Recce", then these will be automatically added or removed from the set. + -- DetectionSetGroup:FilterStart() + -- + -- -- This command defines the reconnaissance network. + -- -- It will group any detected ground enemy targets within a radius of 1km. + -- -- It uses the DetectionSetGroup, which defines the set of reconnaissance groups to detect for enemy ground targets. + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 1000 ) + -- + -- -- Setup the A2A dispatcher, and initialize it. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- + -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **DetectionSetGroup**. + -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with `"CCCP Recce"` to be included in the set. + -- **DetectionSetGroup** is then calling `FilterStart()`, which is starting the dynamic filtering or inclusion of these groups. + -- Note that any destroy or new spawn of a group having a name, starting with the above prefix, will be removed or added to the set. + -- + -- Then a new detection object is created from the class `DETECTION_AREAS`. A grouping radius of 1000 meters (1km) is choosen. + -- + -- The `Detection` object is then passed to the @{#AI_A2G_DISPATCHER.New}() method to indicate the reconnaissance network + -- configuration and setup the A2G defense detection mechanism. + -- + -- ### 1.2. Setup the A2G dispatcher for both a red and blue coalition. + -- + -- Following the above described procedure, you'll need to create for each coalition an separate detection network, and a separate A2G dispatcher. + -- Ensure that while doing so, that you name the objects differently both for red and blue coalition. + -- + -- For example like this for the red coalition: + -- + -- DetectionRed = DETECTION_AREAS:New( DetectionSetGroupRed, 1000 ) + -- A2GDispatcherRed = AI_A2G_DISPATCHER:New( DetectionRed ) + -- + -- And for the blue coalition: + -- + -- DetectionBlue = DETECTION_AREAS:New( DetectionSetGroupBlue, 1000 ) + -- A2GDispatcherBlue = AI_A2G_DISPATCHER:New( DetectionBlue ) + -- + -- + -- Note: Also the SET_GROUP objects should be created for each coalition separately, containing each red and blue recce respectively! + -- + -- ### 1.3. Define the enemy ground target **grouping radius**, in case you use DETECTION_AREAS: + -- + -- The target grouping radius is a property of the DETECTION_AREAS class, that was passed to the AI_A2G_DISPATCHER:New() method, + -- but can be changed. The grouping radius should not be too small, but also depends on the types of ground forces and the way you want your mission to evolve. + -- A large radius will mean large groups of enemy ground targets, while making smaller groups will result in a more fragmented defense system. + -- Typically I suggest a grouping radius of 1km. This is the right balance to create efficient defenses. + -- + -- Note that detected targets are constantly re-grouped, that is, when certain detected enemy ground units are moving further than the group radius, + -- then these units will become a separate area being detected. This may result in additional defenses being started by the dispatcher! + -- So don't make this value too small! Again, I advise about 1km or 1000 meters. + -- + -- ## 2. Setup (a) **Defense Coordinate(s)**. + -- + -- As explained above, defense coordinates are the center of your defense operations. + -- The more threat to the defense coordinate, the higher it is likely a defensive action will be launched. + -- + -- Find below an example how to add defense coordinates: + -- + -- -- Add defense coordinates. + -- A2GDispatcher:AddDefenseCoordinate( "HQ", GROUP:FindByName( "HQ" ):GetCoordinate() ) + -- + -- In this example, the coordinate of a group called `"HQ"` is retrieved, using `:GetCoordinate()` + -- This returns a COORDINATE object, pointing to the first unit within the GROUP object. + -- + -- The method @{#AI_A2G_DISPATCHER.AddDefenseCoordinate}() adds a new defense coordinate to the `A2GDispatcher` object. + -- The first parameter is the key of the defense coordinate, the second the coordinate itself. + -- + -- Later, a COORDINATE_UNIT will be added to the framework, which can be used to assign "moving" coordinates to an A2G dispatcher. + -- + -- **REMEMBER!** + -- + -- - **Defense coordinates are the center of the A2G dispatcher defense system!** + -- - **You can define more defense coordinates to defend a larger area.** + -- + -- But, there is more to it ... + -- + -- + -- ### 2.1. The **Defense Radius**. + -- + -- The defense radius defines the maximum radius that a defense will be initiated around each defense coordinate. + -- So even when there are targets further away than the defense radius, then these targets won't be engaged upon. + -- By default, the defense radius is set to 100km (100.000 meters), but can be changed using the @{#AI_A2G_DISPATCHER.SetDefenseRadius}() method. + -- Note that the defense radius influences the defense reactivity also! The larger the defense radius, the more reactive the defenses will be. + -- + -- For example: + -- + -- A2GDispatcher:SetDefenseRadius( 30000 ) + -- + -- This defines an A2G dispatcher which will engage on enemy ground targets within 30km radius around the defense coordinate. + -- Note that the defense radius **applies to all defense coordinates** defined within the A2G dispatcher. + -- + -- ### 2.2. The **Defense Reactivity**. + -- + -- There are 5 levels that can be configured to tweak the defense reactivity. As explained above, the threat to a defense coordinate is + -- also determined by the distance of the enemy ground target to the defense coordinate. + -- If you want to have a **low** defense reactivity, that is, the probability that an A2G defense will engage to the enemy ground target, then + -- use the @{#AI_A2G_DISPATCHER.SetDefenseReactivityLow}() method. For medium and high reactivity, use the methods + -- @{#AI_A2G_DISPATCHER.SetDefenseReactivityMedium}() and @{#AI_A2G_DISPATCHER.SetDefenseReactivityHigh}() respectively. + -- + -- Note that the reactivity of defenses is always in relation to the Defense Radius! the shorter the distance, + -- the less reactive the defenses will be in terms of distance to enemy ground targets! + -- + -- For example: + -- + -- A2GDispatcher:SetDefenseReactivityHigh() + -- + -- This defines an A2G dispatcher with high defense reactivity. + -- + -- ## 3. **Squadrons**. + -- + -- The A2G dispatcher works with **Squadrons**, that need to be defined using the different methods available. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, farp or carrier, + -- while defining which helicopter or plane **templates** are being used by the squadron and how many **resources** are available. + -- + -- **Multiple squadrons** can be defined within one A2G dispatcher, each having specific defense tasks and defense parameter settings! + -- + -- Squadrons: + -- + -- * Have name (string) that is the identifier or **key** of the squadron. + -- * Have specific helicopter or plane **templates**. + -- * Are located at **one** airbase, farp or carrier. + -- * Optionally have a **limited set of resources**. The default is that squadrons have **unlimited resources**. + -- + -- The name of the squadron given acts as the **squadron key** in all `A2GDispatcher:SetSquadron...()` or `A2GDispatcher:GetSquadron...()` methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new helicopters or aircraft are taking off from the airfield, farp or carrier (in the air, cold, hot, at the runway). + -- * Control how returning helicopters or aircraft are landing at the airfield, farp or carrier (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new helicopters or aircraft spawned at the airfield, farp or carrier. If there is more than one helicopter or aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of helicopters, planes, amount of resources and payload (weapon configuration) chosen, + -- the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- + -- -- @field #AI_A2G_DISPATCHER AI_A2G_DISPATCHER = { ClassName = "AI_A2G_DISPATCHER", From 2d7d19880f5106fc5f0b2915a44291c6e6155a3b Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 11 Nov 2018 23:55:19 +0100 Subject: [PATCH 035/147] AIRBOSS v0.2.3 group fixed init heading --- Moose Development/Moose/AI/AI_Formation.lua | 1 + Moose Development/Moose/Core/UserFlag.lua | 2 +- .../Moose/Functional/CarrierTrainer.lua | 1385 ++++++++++++++++- .../Moose/Functional/Warehouse.lua | 5 +- Moose Development/Moose/Wrapper/Group.lua | 14 +- 5 files changed, 1330 insertions(+), 77 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index b086e7b70..c6f597ca8 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -127,6 +127,7 @@ AI_FORMATION = { -- @param Wrapper.Unit#UNIT FollowUnit The UNIT leading the FolllowGroupSet. -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. -- @param #string FollowName Name of the escort. +-- @param #string FollowBriefing Briefing. -- @return #AI_FORMATION self function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefing ) --R2.1 local self = BASE:Inherit( self, FSM_SET:New( FollowGroupSet ) ) diff --git a/Moose Development/Moose/Core/UserFlag.lua b/Moose Development/Moose/Core/UserFlag.lua index 88c1d0f60..bef5cefff 100644 --- a/Moose Development/Moose/Core/UserFlag.lua +++ b/Moose Development/Moose/Core/UserFlag.lua @@ -70,7 +70,7 @@ do -- UserFlag -- local BlueVictory = USERFLAG:New( "VictoryBlue" ) -- local BlueVictoryValue = BlueVictory:Get() -- Get the UserFlag VictoryBlue value. -- - function USERFLAG:Get( Number ) --R2.3 + function USERFLAG:Get() --R2.3 return trigger.misc.getUserFlag( self.UserFlagName ) end diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index ef68dfc38..296857418 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -1,6 +1,6 @@ --- **Functional** - (R2.5) - Manages aircraft operations on carriers. -- --- Practice carrier landings. +-- The Moose AIRBOSS class manages recoveries of human pilots and AI aircraft for aircraft carriers. -- -- Features: -- @@ -27,6 +27,7 @@ -- @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 Wrapper.Airbase#AIRBASE airbase Carrier airbase object. -- @field Core.Radio#BEACON beacon Carrier beacon for TACAN and ICLS. -- @field #number TACANchannel TACAN channel. -- @field #string TACANmode TACAN mode, i.e. "X" or "Y". @@ -49,6 +50,7 @@ -- @field #number rwyangle Angle of the runway wrt to carrier "nose". For the Stennis ~ -10 degrees. -- @field #number sterndist Distance in meters from carrier coordinate to the end of the deck. -- @field #number deckheight Height of the deck in meters. +-- @field #number case Recovery case I, II or III. -- @field #table Qmarshal Queue of marshalling aircraft groups. -- @field #table Qpattern Queue of aircraft groups in the landing pattern. -- @extends Core.Fsm#FSM @@ -71,6 +73,7 @@ AIRBOSS = { carrier = nil, carriertype = nil, alias = nil, + airbase = nil, beacon = nil, TACANchannel = nil, TACANmode = nil, @@ -81,7 +84,7 @@ AIRBOSS = { Carrierfreq = nil, registerZone = nil, startZone = nil, - carrierZone = nil, + carrierZone = nil, players = {}, menuadded = {}, Upwind = {}, @@ -95,6 +98,7 @@ AIRBOSS = { rwyangle = -9, sterndist =-100, deckheight = 22, + case = 1, Qpattern = {}, Qmarshal = {}, } @@ -272,13 +276,24 @@ AIRBOSS.GroovePos={ -- @field #number Speed Optimal speed at this point. -- @field #table Checklist Table of checklist text items to display at this point. +--- Marshal and pattern queue items. +-- @type AIRBOSS.Queueitem +-- @field Wrapper.Group#GROUP group Flight group. +-- @field #string groupname Name of the group. +-- @field #number nunits Number of units in group. +-- @field #number stack Altitude in feet. +-- @field #number fuel Fuel state. +-- @field #number time Time the flight was added to the queue. +-- @field Core.UserFlag#USERFLAG flag User flag for triggering events for the flight. +-- @field #boolean ai If true, flight is AI. If false, flight is a human player. + --- Main radio menu. -- @field #table MenuF10 AIRBOSS.MenuF10={} --- Carrier trainer class version. -- @field #string version -AIRBOSS.version="0.2.2" +AIRBOSS.version="0.2.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -339,16 +354,17 @@ function AIRBOSS:New(carriername, alias) -- Set alias. self.alias=alias or carriername - -- Get carrier group template. - local grouptemplate=self.carrier:GetGroup():GetTemplate() - -- TODO: Now I need to get TACAN and ICLS if they were set in the ME. + -- Set carrier airbase object. + self.airbase=AIRBASE:FindByName(carriername) -- Create carrier beacon. self.beacon=BEACON:New(self.carrier) + -- Set up airboss and LSO radios self.Carrierradio=RADIO:New(self.carrier) self.LSOradio=RADIO:New(self.carrier) + -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then self:_InitStennis() elseif self.carriertype==AIRBOSS.CarrierType.VINSON then @@ -376,7 +392,7 @@ function AIRBOSS:New(carriername, alias) -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") self:AddTransition("Running", "Status", "Running") - self:AddTransition("Running", "Stop", "Stopped") + self:AddTransition("*", "Stop", "Stopped") --- Triggers the FSM event "Start" that starts the carrier trainer. Initializes parameters and starts event handlers. @@ -479,6 +495,9 @@ function AIRBOSS:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Land) --self:HandleEvent(EVENTS.Crash) + + -- Time stamp for checking queues. + self.Tqueue=timer.getTime() -- Init status check self:__Status(1) @@ -491,14 +510,27 @@ end -- @param #string To To state. function AIRBOSS:onafterStatus(From, Event, To) - -- Scan carrier zone for new aircraft. - self:_ScanCarrierZone() - + -- Get current time. + local time=timer.getTime() + + -- Update marshal and pattern queue every 30 seconds. + if time-self.Tqueue>30 then + + -- Scan carrier zone for new aircraft. + self:_ScanCarrierZone() + + -- Check marshal and pattern queues. + self:_CheckQueue() + + -- Time stamp. + self.Tqueue=time + end + -- Check player status. self:_CheckPlayerStatus() -- Call status again in 0.25 seconds. - self:__Status(-0.25) + self:__Status(-1) end --- On after Stop event. Unhandle events and stop status updates. @@ -511,91 +543,309 @@ function AIRBOSS:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Land) end + +--- Orbit at a specified position at a specified alititude with a specified speed. +-- @param #AIRBOSS self +function AIRBOSS:_CheckQueue() + + local npattern=0 + local nmarshal=#self.Qmarshal + + for _,_flight in pairs(self.Qpattern) do + local flight=_flight --#AIRBOSS.Queueitem + npattern=npattern+flight.nunits + end + + -- Sort list of player results. + --local _sort=function(a, b) return a.stack0 and npattern<1 then + + local flight=self.Qmarshal[1] --#AIRBOSS.Queueitem + + local Tmarshal=timer.getTime()-flight.time + env.info(string.format("Marshal time of group %s = %d seconds", flight.groupname, Tmarshal)) + + local Tpattern=999 + if npattern>0 then + local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.Queueitem + Tpattern=timer.getTime()-patternflight.time + env.info(string.format("Pattern time of group %s = %d seconds", patternflight.groupname, Tpattern)) + end + + -- Two minutes in pattern at leastand >45 sec interval between pattern flights. + if Tmarshal>120 and Tpattern>45 then + self:_CollapseMarshalStack() + end + + end +end + +--- Collapse marshal stack. +-- @param #AIRBOSS self +-- @param #table queue Queue to print. +-- @param #string name Queue name. +function AIRBOSS:_PrintQueue(queue, name) + + local nqueue=#queue + + local text=string.format("%s Queue:", name) + if nqueue==0 then + text=text.." empty." + else + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.Queueitem + local clock=UTILS.SecondsToClock(flight.time) + text=text..string.format("\n[%d] %s*%d: stack=%d, flag=%d time=%s", i, flight.groupname, flight.nunits, flight.stack, flight.flag:Get(), clock) + end + end + env.info(text) +end + + --- Check if new aircraft arrived -- @param #AIRBOSS self function AIRBOSS:_ScanCarrierZone() - - env.info("FF Scanning Carrier Zone") + --env.info("FF Scanning Carrier Zone") -- Carrier position. local coord=self.carrier:GetCoordinate() -- Scan units in carrier zone. - local _,_,_,unitsin =coord:ScanObjects(10*1000, true, false, false) - --local _,_,_,unitsout=coord:ScanObjects(15*1000, true, false, false) + local _,_,_,unitscan=coord:ScanObjects(30*1000, true, false, false) - for _,_unit in pairs(unitsin) do + -- Inside and outside zones. + local zbig=ZONE_RADIUS:New("Bla1", self.carrier:GetVec2(), 30*1000) + local zsma=ZONE_RADIUS:New("Bla2", self.carrier:GetVec2(), 10*1000) + + -- Check if we scaned already. + if self.unitsout~=nil then + + for _,_unit in pairs(self.unitsout) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Check if this an aircraft and that it is airborn and closing in. + if unit:IsAir() and unit:InAir() and unit:IsInZone(zsma)then + -- TODO: check for correct aircraft types and also helos! + + local group=unit:GetGroup() + local unitname=unit:GetName() + local groupname=group:GetName() + + local text=string.format("In carrier zone: unit=%s group=%s", unitname, groupname) + --env.info(text) + + -- Check that it is not already in one of the queues. + if not (self:_InQueue(self.Qmarshal, group) or self:_InQueue(self.Qpattern, group)) then + + env.info("FF new marshal group="..groupname) + if self:_IsHuman(group) then + self:_MarshalPlayer(group) + else + self:_MarshalAI(group) + end + end + end + end + + end + + -- Get all air(born) units that are currently outside but not inside. + self.unitsout={} + for _,_unit in pairs(unitscan) do local unit=_unit --Wrapper.Unit#UNIT - - if unit:IsAir() then - - local group=unit:GetGroup() - local unitname=unit:GetName() - local groupname=group:GetName() - - local text=string.format("In carrier zone: unit=%s group=%s", unitname, groupname) - --env.info(text) - - if self.Qmarshal[groupname]==nil then - - env.info("FF marshal group="..groupname) - self:_Marshal(group) - - end - + if unit:IsAir() and unit:InAir() and unit:IsInZone(zbig) and not unit:IsInZone(zsma) then + env.info(string.format("Possible incoming unit %s", unit:GetName())) + table.insert(self.unitsout, unit) end - end - ---[[ - for _,_unitin in pairs(unitsin) do - local unitin=_unitin --Wrapper.Unit#UNIT - if unit:IsAir()() then - local text=string.format("Aircraft in carrier zone = ", unit:GetName()) - env.info(text) - end - end -]] - + end end + --- Orbit at a specified position at a specified alititude with a specified speed. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Group -function AIRBOSS:_Marshal(group) +function AIRBOSS:_MarshalPlayer(group, stack) + -- Flight group name. local groupname=group:GetName() - local Coord=self.carrier:GetCoordinate() - local Altitude=UTILS.FeetToMeters(2000) + -- Number of full marshal stacks. + local nstacks=#self.Qmarshal + + -- Add group to marshal stack. + self:_AddMarshallGroup(group, nstacks+1) + + --TODO: playerData set +end + +--- Tell AI to orbit at a specified position at a specified alititude with a specified speed. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Group +function AIRBOSS:_MarshalAI(group) + + -- Flight group name. + local groupname=group:GetName() + + -- Number of full marshal stacks. + local nstacks=#self.Qmarshal + + -- Current carrier position. + local Carrier=self.carrier:GetCoordinate() + + -- Aircraft speed when flying the pattern. local Speed=UTILS.KnotsToMps(272) - local DCSTask={} - DCSTask.id="ControlledTask" - DCSTask.params={} - DCSTask.params.task=group:TaskOrbit(Coord, Altitude, Speed) - DCSTask.params.stopCondition={userFlag=groupname, userFlagValue=1} - - -- Set waypoint landing on Carrier. - local wp={} - wp[1]=self.carrier:GetCoordinate():SetAltitude(Altitude):WaypointAirTurningPoint(nil, Speed, {DCSTask}, string.format("Marshal @ %d ft %d knots", Altitude, Speed)) - wp[2]=self.carrier:GetCoordinate():WaypointAirLanding(Speed, AIRBASE:FindByName(self.carrier:GetName()), nil, "Landing") - group:WayPointInitialize(wp) - group:Route(wp, 0) + --- Create a DCS task to orbit at a certain altitude. + local function _taskorbit(coord, alt, speed, stopflag) - self.Qmarshal[groupname]=group + local DCSTask={} + DCSTask.id="ControlledTask" + DCSTask.params={} + DCSTask.params.task=group:TaskOrbit(coord, alt, speed) + DCSTask.params.stopCondition={userFlag=groupname, userFlagValue=stopflag} + + return DCSTask + end + + -- Waypoints array. + local wp={} + + -- Set up waypoints including collapsing the stack. + local n=1 -- Waypoint counter. + for i=nstacks+1,1,-1 do + --env.info("FF i="..i) + + -- Pattern altitude. + local Altitude=UTILS.FeetToMeters((i-1)*1000+2000) + + -- Orbit task. + local TaskOrbit=_taskorbit(Carrier, Altitude, Speed, i-1) + + -- Waypoint description. + local text=string.format("Marshal @ %d ft, %d knots", UTILS.MetersToFeet(Altitude), UTILS.MpsToKnots(Speed)) + env.info(string.format("FF %s: %s stopFlag=%d", groupname, text, i-1)) + + -- Waypoint. + wp[n]=Carrier:SetAltitude(Altitude):WaypointAirTurningPoint(nil, Speed, {TaskOrbit}, text) + + -- Increase counter. + n=n+1 + end + + -- Landing waypoint. + wp[#wp+1]=Carrier:WaypointAirLanding(Speed, self.airbase, nil, "Landing") + + -- Add group to marshal stack. + self:_AddMarshallGroup(group, nstacks+1) + + -- Reinit waypoints. + group:WayPointInitialize(wp) + + -- Route group. + group:Route(wp, 0) end - - ---- Check if new aircraft group arrived. +--- Add a flight group to the marshal stack. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. -function AIRBOSS:_AddGroupMarshall(group) +-- @param #number flagvalue Initial user flag value. +function AIRBOSS:_AddMarshallGroup(group, flagvalue) + + -- Flight group name local groupname=group:GetName() - self.Qmarshal[groupname]=group + -- Queue table item. + local qitem={} --#AIRBOSS.Queueitem + qitem.group=group + qitem.groupname=group:GetName() + qitem.nunits=#group:GetUnits() + qitem.fuel=group:GetFuelMin() + qitem.time=timer.getTime() + qitem.stack=(flagvalue-1)*1000+2000 --TODO: Case III + qitem.flag=USERFLAG:New(groupname) + qitem.flag:Set(flagvalue) + qitem.ai=not self:_IsHuman(group) + + -- Pressure. + local hPa2inHg=0.0295299830714 + local P=self.carrier:GetCoordinate():GetPressure()*hPa2inHg + + -- Marshal message. + local text=string.format("XYZ, Case 1, BRC is 000, hold at %d. Expected Charlie Time XX.\n", qitem.stack) + text=text..string.format("Altimeter %.2f. Report see me.") + MESSAGE:New(text, 30):ToAll() + + -- Add to marshal queue. + table.insert(self.Qmarshal, qitem) end +--- Collapse marshal stack. +-- @param #AIRBOSS self +function AIRBOSS:_CollapseMarshalStack() + + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.Queueitem + local flagvalue=flight.flag:Get() + flight.flag:Set(flagvalue-1) + end + + local flight=self.Qmarshal[1] --#AIRBOSS.Queueitem + env.info(string.format("New pattern flight %s.", flight.groupname)) + + -- TODO: better message. + MESSAGE:New(string.format("Marshal, %s, you are cleared for Case I recovery pattern!", flight.groupname), 15):ToAll() + + -- Time stamp. + flight.time=timer.getTime() + + -- Add flight to pattern queue + table.insert(self.Qpattern, flight) + + -- Remove flight from marshal queue. + table.remove(self.Qmarshal, 1) +end + +--- Checks if a group has a human player. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #boolean If true, human player inside group. +function AIRBOSS:_IsHuman(group) + + local units=group:GetUnits() + + for _,_unit in pairs(units) do + local playerunit=self:_GetPlayerUnitAndName(_unit:GetName()) + if playerunit then + return true + end + end + + return false +end + +--- Check if a group is in the queue. +-- @param #AIRBOSS self +-- @param #table queue The queue to check. +-- @param Wrapper.Group#GROUP group +-- @return #boolean If true, group is in the queue. False otherwise. +function AIRBOSS:_InQueue(queue, group) + local name=group:GetName() + for _,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.Queueitem + if name==flight.groupname then + return true + end + end + return false +end + + --- Check current player status. -- @param #AIRBOSS self function AIRBOSS:_CheckPlayerStatus() @@ -635,6 +885,7 @@ function AIRBOSS:_CheckPlayerStatus() end if playerData.step==0 and unit:InAir() then + -- New approach. self:_NewRound(playerData) @@ -643,22 +894,22 @@ function AIRBOSS:_CheckPlayerStatus() playerData.step=90 self.groovedebug=false end - elseif playerData.step == 1 then + elseif playerData.step==1 then -- Entering the pattern. self:_Start(playerData) - elseif playerData.step == 2 then + elseif playerData.step==2 then -- Upwind leg. self:_Upwind(playerData) - elseif playerData.step == 3 then + elseif playerData.step==3 then -- Early break. self:_Break(playerData, "early") - elseif playerData.step == 4 then + elseif playerData.step==4 then -- Late break. self:_Break(playerData, "late") - elseif playerData.step == 5 then + elseif playerData.step==5 then -- Abeam position. self:_Abeam(playerData) - elseif playerData.step == 6 then + elseif playerData.step==6 then -- Check long down wind leg. self:_CheckForLongDownwind(playerData) -- At the ninety. @@ -744,7 +995,7 @@ function AIRBOSS:OnEventBirth(EventData) end end ---- Carrier trainer event handler for event land. +--- Airboss event handler for event land. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData function AIRBOSS:OnEventLand(EventData) @@ -795,7 +1046,33 @@ function AIRBOSS:OnEventLand(EventData) -- Call trapped function in 3 seconds to make sure we did not bolter. SCHEDULER:New(nil, self._Trapped,{self, playerData, coord}, 3) - end + end + + if self:_InQueue(self.Qpattern, EventData.IniGroup) then + self:_RemoveQueue(self.Qpattern, EventData.IniGroup) + end + +end + +--- Airboss event handler for event land. +-- @param #AIRBOSS self +-- @param #table queue The queue from which the group will be removed. +-- @param Wrapper.Group#GROUP group Group that will be removed from queue. +function AIRBOSS:_RemoveQueue(queue, group) + + local name=group:GetName() + + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.Queueitem + if flight.groupname==name then + flight.nunits=flight.nunits-1 + if flight.nunits==0 then + env.info(string.format("FF removing group %s from queue.", name)) + table.remove(queue, i) + end + end + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2800,3 +3077,963 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) end end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + +--- **Functional** - (R2.5) - Rescue helo. +-- +-- Recue helicopter on an aircraft carrier +-- +-- Features: +-- +-- * Formation with carrier. +-- * Automatic respawning on empty fuel. +-- +-- Please not that his class is work in progress and in an **alpha** stage. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- @module Functional.RescueHelo +-- @image MOOSE.JPG + +--- RESCUEHELO class. +-- @type RESCUEHELO +-- @field #string ClassName Name of the class. +-- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. +-- @field #string carriertype Carrier type. +-- @field #string helogroupname Name of the late activated helo template group. +-- @field Wrapper.Group#GROUP helo Helo group. +-- @field #number takeoff Takeoff type. +-- @field Wrapper.Airbase#AIRBASE airbase The airbase object of the carrier. +-- @field Core.Set#SET_GROUP followset Follow group set. +-- @field AI.AI_Formation#AI_FORMATION formation AI_FORMATION object. +-- @field #number lowfuel Low fuel threshold of helo in percent. +-- @extends Core.Fsm#FSM + +--- Rescue Helo +-- +-- === +-- +-- ![Banner Image](..\Presentations\RESCUEHELO\RescueHelo_Main.png) +-- +-- # Recue helo +-- +-- bla bla +-- +-- @field #RESCUEHELO +RESCUEHELO = { + ClassName = "RESCUEHELO", + carrier = nil, + carriertype = nil, + helogroupname = nil, + helo = nil, + airbase = nil, + takeoff = nil, + followset = nil, + formation = nil, + lowfuel = nil, +} + +--- Class version. +-- @field #string version +RESCUEHELO.version="0.9.0" + +-- TODO: Add rescue event. +-- TODO: Make offset input parameter. + +--- Constructor. +-- @param #RESCUEHELO self +-- @param Wrapper.Unit#UNIT carrierunit Carrier unit. +-- @param #string helogroupname Name of the late activated rescue helo template group. +-- @return #RESCUEHELO RESCUEHELO object. +function RESCUEHELO:New(carrierunit, helogroupname) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #RESCUEHELO + + if type(carrierunit)=="string" then + self.carrier=UNIT:FindByName(carrierunit) + else + self.carrier=carrierunit + end + + -- Carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Helo group name. + self.helogroupname=helogroupname + + -- Home airbase of helo + self.airbase=AIRBASE:FindByName(self.carrier:GetName()) + + -- Init defaults. + self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) + self:SetTakeoffHot() + self:SetLowFuelThreshold(10) + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("Running", "RTB", "Returning") + self:AddTransition("Returning", "Status", "*") + self:AddTransition("Running", "Status", "*") + self:AddTransition("Running", "Stop", "Stopped") + + + --- Triggers the FSM event "Start" that starts the carrier trainer. Initializes parameters and starts event handlers. + -- @function [parent=#RESCUEHELO] Start + -- @param #RESCUEHELO self + + --- Triggers the FSM event "Start" after a delay that starts the carrier trainer. Initializes parameters and starts event handlers. + -- @function [parent=#RESCUEHELO] __Start + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "RTB" that sends the helo home. + -- @function [parent=#RESCUEHELO] RTB + -- @param #RESCUEHELO self + + --- Triggers the FSM event "RTB" that sends the helo home after a delay. + -- @function [parent=#RESCUEHELO] __RTB + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped. + -- @function [parent=#RESCUEHELO] Stop + -- @param #RESCUEHELO self + + --- Triggers the FSM event "Stop" that stops the rescue helo after a delay. Event handlers are stopped. + -- @function [parent=#RESCUEHELO] __Stop + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + return self + +end + +--- Set low fuel state of helo. When fuel is below this threshold, the helo will RTB or be respawned if takeoff type is in air. +-- @param #RESCUEHELO self +-- @param #number threshold Low fuel threshold in percent. Default 10. +-- @return #RESCUEHELO self +function RESCUEHELO:SetLowFuelThreshold(threshold) + self.lowfuel=threshold or 10 + return self +end + +--- Set home airbase of the helo. Default is the carrier. +-- @param #RESCUEHELO self +-- @param Wrapper.Airbase#AIRBASE airbase Homebase of helo. +-- @return #RESCUEHELO self +function RESCUEHELO:SetHomeBase(airbase) + self.airbase=airbase + return self +end + +--- Set takeoff type. +-- @param #RESCUEHELO self +-- @param #number takeofftype Takeoff type. +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoff(takeofftype) + self.takeoff=takeofftype + return self +end + +--- Set takeoff with engines running (hot). +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffHot() + self:SetTakeoff(SPAWN.Takeoff.Hot) + return self +end + +--- Set takeoff with engines off (cold). +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffCold() + self:SetTakeoff(SPAWN.Takeoff.Cold) + return self +end + +--- Set takeoff in air near the carrier. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffAir() + self:SetTakeoff(SPAWN.Takeoff.Air) + return self +end + + +--- Check if tanker is returning to base. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is returning to base. +function RESCUEHELO:IsReturning() + return self:is("Returning") +end + +--- Check if tanker is operating. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is operating. +function RESCUEHELO:IsRunning() + return self:is("Running") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #RESCUEHELO self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO:onafterStart(From, Event, To) + + -- Events are handled my MOOSE. + self:I(string.format("Starting Rescue Helo Formation v%s for carrier unit %s of type %s.", RESCUEHELO.version, self.carrier:GetName(), self.carriertype)) + + -- Handle events. + --self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Land) + --self:HandleEvent(EVENTS.Crash) + + -- Offset [meters] in the direction of travelling. Positive values are in front of Mother. + local OffsetX=200 + -- Offset [meters] perpendicular to travelling. Positive = Starboard (right of Mother), negative = Port (left of Mother). + local OffsetZ=200 + -- Offset altitude. Should (obviously) always be positve. + local OffsetY=70 + + -- Delay before formation is started. + local delay=120 + + -- Spawn helo. + local Spawn=SPAWN:New(self.helogroupname):InitUnControlled(false) + + -- Spawn in air or at airbase. + if self.takeoff==SPAWN.Takeoff.Air then + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Spawn distance behind carrier. + local dist=UTILS.NMToMeters(0.2) + + -- Coordinate behind the carrier + local Carrier=self.carrier:GetCoordinate():SetAltitude(OffsetY):Translate(dist, hdg) + + -- Orientation of spawned group. + Spawn:InitHeading(hdg) + + -- Spawn at coordinate. + self.helo=Spawn:SpawnFromCoordinate(Carrier) + + -- Start formation in 1 seconds + delay=1 + + else + + -- Spawn at airbase. + self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) + + if self.takeoff==SPAWN.Takeoff.Runway then + delay=5 + elseif self.takeoff==SPAWN.Takeoff.Hot then + delay=30 + elseif self.takeoff==SPAWN.Takeoff.Cold then + delay=60 + end + + end + + -- Set of group(s) to follow Mother. + self.followset=SET_GROUP:New() + self.followset:AddGroup(self.helo) + + -- Get initial fuel. + self.HeloFuel0=self.helo:GetFuel() + + -- Define AI Formation object. + self.formation=AI_FORMATION:New(self.carrier, self.followset, "Helo Formation with Carrier", "Follow Carrier at given parameters.") + + -- Formation parameters. + self.formation:FormationCenterWing(-OffsetX, 50, math.abs(OffsetY), 50, OffsetZ, 50) + + -- Start formation FSM. + self.formation:__Start(delay) + + -- Start uncontrolled helo. + --HeloSpawn:StartUncontrolled(120) + + -- Init status check + self:__Status(1) + +end + +--- On after Status event. Checks player status. +-- @param #RESCUEHELO self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Get relative fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712) + local fuel=self.helo:GetFuel()/self.HeloFuel0*100 + + -- Report current fuel. + local text=string.format("Rescue Helo %s: state=%s fuel=%.1f", self.helo:GetName(), self:GetState(), fuel) + self:I(text) + + -- If fuel < threshold ==> send helo to home base! + if fuel Event --> To State + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("Running", "RTB", "Returning") + self:AddTransition("Running", "Status", "*") + self:AddTransition("Returning", "Status", "*") + self:AddTransition("Running", "Stop", "Stopped") + + + --- Triggers the FSM event "Start" that starts the carrier trainer. Initializes parameters and starts event handlers. + -- @function [parent=#CARRIERTANKER] Start + -- @param #CARRIERTANKER self + + --- Triggers the FSM event "Start" after a delay that starts the carrier trainer. Initializes parameters and starts event handlers. + -- @function [parent=#CARRIERTANKER] __Start + -- @param #CARRIERTANKER self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "RTB" that sends the tanker home. + -- @function [parent=#CARRIERTANKER] RTB + -- @param #CARRIERTANKER self + + --- Triggers the FSM event "RTB" that sends the tanker home after a delay. + -- @function [parent=#CARRIERTANKER] __RTB + -- @param #CARRIERTANKER self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop" that stops the carrier trainer. Event handlers are stopped. + -- @function [parent=#CARRIERTANKER] Stop + -- @param #CARRIERTANKER self + + --- Triggers the FSM event "Stop" that stops the carrier trainer after a delay. Event handlers are stopped. + -- @function [parent=#CARRIERTANKER] __Stop + -- @param #CARRIERTANKER self + -- @param #number delay Delay in seconds. + + return self +end + +--- Set the speed the tanker flys in its orbit pattern. +-- @param #CARRIERTANKER self +-- @param #number speed Tanker speed in knots. +-- @return #CARRIERTANKER self +function CARRIERTANKER:SetSpeed(speed) + self.speed=UTILS.KnotsToMps(speed) + return self +end + +--- Set orbit pattern altitude of the tanker. +-- @param #CARRIERTANKER self +-- @param #number altitude Tanker altitude in feet. +-- @return #CARRIERTANKER self +function CARRIERTANKER:SetAltitude(altitude) + self.altitude=UTILS.FeetToMeters(altitude) + return self +end + +--- Set race-track distances. +-- @param #CARRIERTANKER self +-- @param #number distbow Distance [NM] in front of the carrier. Default 6 NM. +-- @param #number diststern Distance [NM] behind the carrier. Default 8 NM. +-- @return #CARRIERTANKER self +function CARRIERTANKER:SetRacetrackDistances(distbow, diststern) + self.distBow=UTILS.NMToMeters(distbow or 6) + self.distStern=-UTILS.NMToMeters(diststern or 8) + return self +end + +--- Set pattern update interval. Note that this update causes a slight disruption in the race track pattern. +-- Therefore, the interval should be as long as possible but short enough to keep the tanker overhead the carrier. +-- @param #CARRIERTANKER self +-- @param #number interval Interval in minutes. Default is every 30 minutes. +-- @return #CARRIERTANKER self +function CARRIERTANKER:SetPatternUpdateInterval(interval) + self.dTupdate=(interval or 30)*60 + return self +end + +--- Set low fuel state of tanker. When fuel is below this threshold, the tanker will RTB or be respawned if takeoff type is in air. +-- @param #CARRIERTANKER self +-- @param #number threshold Low fuel threshold in percent. Default 10. +-- @return #CARRIERTANKER self +function CARRIERTANKER:SetLowFuelThreshold(threshold) + self.lowfuel=threshold or 10 + return self +end + +--- Set home airbase of the tanker. Default is the carrier. +-- @param #CARRIERTANKER self +-- @param Wrapper.Airbase#AIRBASE airbase +-- @return #CARRIERTANKER self +function CARRIERTANKER:SetHomeBase(airbase) + self.airbase=airbase + return self +end + +--- Set takeoff type. +-- @param #CARRIERTANKER self +-- @param #number takeofftype Takeoff type. +-- @return #CARRIERTANKER self +function CARRIERTANKER:SetTakeoff(takeofftype) + self.takeoff=takeofftype + return self +end + +--- Set takeoff with engines running (hot). +-- @param #CARRIERTANKER self +-- @return #CARRIERTANKER self +function CARRIERTANKER:SetTakeoffHot() + self:SetTakeoff(SPAWN.Takeoff.Hot) + return self +end + +--- Set takeoff with engines off (cold). +-- @param #CARRIERTANKER self +-- @return #CARRIERTANKER self +function CARRIERTANKER:SetTakeoffCold() + self:SetTakeoff(SPAWN.Takeoff.Cold) + return self +end + +--- Set takeoff in air at pattern altitude 30 NM behind the carrier. +-- @param #CARRIERTANKER self +-- @return #CARRIERTANKER self +function CARRIERTANKER:SetTakeoffAir() + self:SetTakeoff(SPAWN.Takeoff.Air) + return self +end + + +--- Check if tanker is returning to base. +-- @param #CARRIERTANKER self +-- @return #boolean If true, tanker is returning to base. +function CARRIERTANKER:IsReturning() + return self:is("Returning") +end + +--- Check if tanker is operating. +-- @param #CARRIERTANKER self +-- @return #boolean If true, tanker is operating. +function CARRIERTANKER:IsRunning() + return self:is("Running") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #CARRIERTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CARRIERTANKER:onafterStart(From, Event, To) + + -- Info on start. + self:I(string.format("Starting Carrier Tanker v%s for carrier unit %s of type %s for tanker group %s.", CARRIERTANKER.version, self.carrier:GetName(), self.carriertype, self.tankergroupname)) + + -- Handle events. + self:HandleEvent(EVENTS.EngineShutdown) + --TODO: Handle event crash and respawn. + + -- Spawn tanker. + local Spawn=SPAWN:New(self.tankergroupname):InitUnControlled(false) + + -- Spawn on carrier. + if self.takeoff==SPAWN.Takeoff.Air then + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + local dist=UTILS.NMToMeters(20) + + -- Coordinate behind the carrier + local Carrier=self.carrier:GetCoordinate():SetAltitude(self.altitude):Translate(-dist, hdg) + + -- Orientation of spawned group. + Spawn:InitHeading(hdg) + + -- Spawn at coordinate. + self.tanker=Spawn:SpawnFromCoordinate(Carrier) + + self:_InitRoute(15, 1, 2) + else + + -- Spawn tanker at airbase. + self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) + self:_InitRoute(30, 10, 1) + + end + + -- Init status check. + self:__Status(10) +end + +--- On after Status event. Checks player status. +-- @param #CARRIERTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CARRIERTANKER:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Get fuel of tanker. + local fuel=self.tanker:GetFuel()*100 + local text=string.format("Tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) + self:I(text) + + + if self:IsRunning() then + + -- Check fuel. + if fuelself.dTupdate then + self:_PatternUpdate() + end + + end + end + + end + + -- Call status again in 1 minute. + self:__Status(-60) +end + +--- On after Stop event. Unhandle events and stop status updates. +-- @param #CARRIERTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CARRIERTANKER:onafterStop(From, Event, To) + self:UnHandleEvent(EVENTS.EngineShutdown) + --self:UnHandleEvent(EVENTS.Land) +end + +--- On before RTB event. Check if takeoff type is air and if so respawn the tanker and deny RTB transition. +-- @param #CARRIERTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean If true, transition is allowed. +function CARRIERTANKER:onbeforeRTB(From, Event, To) + + if self.takeoff==SPAWN.Takeoff.Air then + + -- Debug message. + local text=string.format("Respawning tanker %s.", self.tanker:GetName()) + self:I(text) + + -- Respawn tanker. + self.tanker:InitHeading(self.tanker:GetHeading()) + self.tanker=self.tanker:Respawn(nil, true) + + -- Update Pattern in 2 seconds. Need to give a bit time so that the respawned group is in the game. + SCHEDULER:New(nil, self._PatternUpdate, {self}, 2) + + -- Deny transition to RTB. + return false + end + + return true +end + +--- On after RTB event. Send tanker back to carrier. +-- @param #CARRIERTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CARRIERTANKER:onafterRTB(From, Event, To) + + -- Debug message. + local text=string.format("Tanker %s returning to airbase %s.", self.tanker:GetName(), self.airbase:GetName()) + self:I(text) + + local waypoints={} + + -- Set landingwaypoint + local wp=self.carrier:GetCoordinate():WaypointAirLanding(300, self.airbase, nil, "Landing") + table.insert(waypoints, wp) + + -- Initialize WP and route tanker. + self.tanker:WayPointInitialize(waypoints) + + -- Set task. + self.tanker:Route(waypoints, 1) +end + + +--- Event handler for engine shutdown of carrier tanker. +-- Respawn tanker group once it landed because it was out of fuel. +-- @param #CARRIERTANKER self +-- @param Core.Event#EVENTDATA EventData Event data. +function CARRIERTANKER:OnEventEngineShutdown(EventData) + + local group=EventData.IniGroup --Wrapper.Group#GROUP + + if group:IsAlive() then + + -- Group name. When spawning it will have #001 attached. + local groupname=group:GetName() + + if groupname:match(self.tankergroupname) then + + -- Debug info. + self:I(string.format("CARIERTANKER: Respawning group %s.", group:GetName())) + + -- Respawn tanker. + self.tanker=group:RespawnAtCurrentAirbase() + + --group:StartUncontrolled(60) + + -- Initial route. + self:_InitRoute() + end + + end +end + + +--- Init waypoint after spawn. +-- @param #CARRIERTANKER self +-- @param #number dist Distance [NM] of initial waypoint astern carrier. Default 30 NM. +-- @param #number Tstart Time in minutes before the tanker starts its pattern. Default 10 min. +-- @param #number delay Delay before routing in seconds. Default 1 second. +function CARRIERTANKER:_InitRoute(dist, Tstart, delay) + + -- Defaults. + dist=UTILS.NMToMeters(dist or 30) + Tstart=(Tstart or 10)*60 + delay=delay or 1 + + -- Debug message. + self:I(string.format("Initializing route for tanker %s.", self.tanker:GetName())) + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- First waypoint is 50 km behind the boat. + local p=Carrier:Translate(-dist, hdg):SetAltitude(self.altitude) + + -- Debug mark + p:MarkToAll(string.format("Init WP: alt=%d ft, speed=%d kts", UTILS.MetersToFeet(self.altitude), UTILS.MpsToKnots(self.speed))) + + -- Waypoints. + local wp={} + wp[1]=Carrier:WaypointAirTakeOffParking() + wp[2]=p:WaypointAirTurningPoint(nil, self.speed, nil, "Stern") + + -- Set route. + self.tanker:Route(wp, delay) + + -- No update yet. + self.Tupdate=nil + + -- Update pattern in ~10 minutes. + SCHEDULER:New(nil, self._PatternUpdate, {self}, Tstart) +end + + +--- Function to update the race-track pattern of the tanker wrt to the carrier position. +-- @param #CARRIERTANKER self +function CARRIERTANKER:_PatternUpdate() + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + -- Define race-track pattern. + local p1=Carrier:SetAltitude(self.altitude):Translate(self.distStern, hdg) + local p2=Carrier:SetAltitude(self.altitude):Translate(self.distBow, hdg) + + -- Set orbit task. + local taskorbit=self.tanker:TaskOrbit(p1, self.altitude, self.speed, p2) + + -- New waypoint. + local p0=self.tanker:GetCoordinate():Translate(1000, self.tanker:GetHeading()) + + -- Debug markers. + if self.Debug then + p0:MarkToAll("p0") + p1:MarkToAll("p1") + p2:MarkToAll("p2") + end + + -- Debug message. + self:I(string.format("Updating tanker %s orbit.", self.tanker:GetName())) + + -- Waypoints array. + local waypoints={} + + -- New waypoint with orbit pattern task. + local wp=p0:WaypointAirTurningPoint(nil, self.speed, {taskorbit}, "Tanker Orbit") + waypoints[1]=wp + + -- Initialize WP and route tanker. + self.tanker:WayPointInitialize(waypoints) + + -- Task combo. + local tasktanker = self.tanker:EnRouteTaskTanker() + local taskroute = self.tanker:TaskRoute(waypoints) + local taskcombo = self.tanker:TaskCombo({tasktanker, taskroute}) + + -- Set task. + self.tanker:SetTask(taskcombo, 1) + + -- Set update time. + self.Tupdate=timer.getTime() +end diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 2802ecc52..55ee5e9d8 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1798,7 +1798,10 @@ function WAREHOUSE:New(warehouse, alias) -- Check if just a string was given and convert to static. if type(warehouse)=="string" then - warehouse=STATIC:FindByName(warehouse, true) + warehouse=GROUP:FindByName(warehouse) + if warehouse==nil then + warehouse=STATIC:FindByName(warehouse, true) + end end -- Nil check. diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 6c6061e7d..c93866343 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1482,6 +1482,17 @@ function GROUP:Respawn( Template, Reset ) if not Template then Template = self:GetTemplate() end + + -- Get correct heading. + local function _Heading(course) + local h + if course<=180 then + h=math.rad(course) + else + h=-math.rad(360-course) + end + return h + end if self:IsAlive() then local Zone = self.InitRespawnZone -- Core.Zone#ZONE @@ -1515,7 +1526,8 @@ function GROUP:Respawn( Template, Reset ) Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position. Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position. - Template.units[UnitID].heading = self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading() + Template.units[UnitID].heading = _Heading(self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading()) + Template.units[UnitID].psi = -Template.units[UnitID].heading self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) end end From 1b691e588798985c325caafc1d6bcbae5eeaa812 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 12 Nov 2018 19:35:32 +0100 Subject: [PATCH 036/147] Documentation --- .../Moose/AI/AI_A2G_Dispatcher.lua | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 26eda3fc5..d0c8a273e 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -388,6 +388,7 @@ do -- AI_A2G_DISPATCHER -- -- - **Defense coordinates are the center of the A2G dispatcher defense system!** -- - **You can define more defense coordinates to defend a larger area.** + -- - **Detected enemy ground targets are not immediately engaged, but are engaged with a reactivity or probability calculation!** -- -- But, there is more to it ... -- @@ -448,8 +449,93 @@ do -- AI_A2G_DISPATCHER -- * Control the **grouping** of new helicopters or aircraft spawned at the airfield, farp or carrier. If there is more than one helicopter or aircraft to be spawned, these may be grouped. -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of helicopters, planes, amount of resources and payload (weapon configuration) chosen, -- the mission designer can choose to increase or reduce the amount of planes spawned. - -- -- + -- The method @{#AI_A2G_DISPATCHER.SetSquadron}() defines for you a new squadron. + -- The provided parameters are the squadron name, airbase name and a list of template prefixe, and a number that indicates the amount of resources. + -- + -- For example, this defines 3 new squadrons: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50" }, 10 ) + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50" }, 10 ) + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50" }, 10 ) + -- + -- The latter 2 will depart from FARPs, which bare the name `"CAS"` and `"BAI"`. + -- + -- + -- ### 3.1. Squadrons **Tasking**. + -- + -- Squadrons can be commanded to execute 3 types of tasks, as explained above: + -- + -- - SEAD: Suppression of Air Defenses, which are ground targets that have medium or long range radar emitters. + -- - CAS : Close Air Support, when there are enemy ground targets close to friendly units. + -- - BAI : Battlefield Air Interdiction, which are targets further away from the frond-line. + -- + -- You need to configure each squadron which task types you want it to perform. Read on ... + -- + -- ### 3.2. Squadrons enemy ground target **Engagement**. + -- + -- There are two ways how targets can be engaged: directly upon call from the airfield, farp or carrier, or through a patrol. + -- + -- Patrols are extremely handy, as these will airborne your helicopters or airplanes in advance. They will patrol in defined zones outlined, + -- and will engage with the targets once commanded. If the patrol zone is close enough to the enemy ground targets, then the time required + -- to engage is heavily minimized! + -- + -- However; patrols come with a side effect: since your resources are airborne, they will be vulnerable to incoming air attacks from the enemy. + -- + -- The mission designer needs to carefully balance the need for patrols or the need for engagement on call from the airfields. + -- + -- ### 3.3. Squadron **on call engagement**. + -- + -- So to make squadrons engage targets from the airfields, use the following methods: + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSead}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCas}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBai}() method. + -- + -- Note that for the tasks, specific helicopter or airplane templates are required to be used, which you can configure using your mission editor. + -- Especially the payload (weapons configuration) is important to get right. + -- + -- For example, the following will define for the squadrons different tasks: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- A2GDispatcher:SetSquadronSead( "Maykop SEAD", 120, 250 ) + -- + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- A2GDispatcher:SetSquadronCas( "Maykop CAS", 120, 250 ) + -- + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- A2GDispatcher:SetSquadronBai( "Maykop BAI", 120, 250 ) + -- + -- ### 3.4. Squadron **on patrol engagement**. + -- + -- Squadrons can be setup to patrol in the air near the engagement hot zone. + -- When needed, the A2G defense units will be close to the battle area, and can engage quickly. + -- + -- So to make squadrons engage targets from a patrol zone, use the following methods: + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrol}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrol}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrol}() method. + -- + -- Because a patrol requires more parameters, the following methods must be used to fine-tune the patrols for each squadron. + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrolInterval}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrolInterval}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrolInterval}() method. + -- + -- Here an example to setup patrols of various task types: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Maykop SEAD", PatrolZone, 300, 500, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop SEAD", 2, 30, 60, 1, "SEAD" ) + -- + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- A2GDispatcher:SetSquadronCasPatrol( "Maykop CAS", PatrolZone, 600, 700, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop CAS", 2, 30, 60, 1, "CAS" ) + -- + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Maykop BAI", PatrolZone, 800, 900, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop BAI", 2, 30, 60, 1, "BAI" ) -- -- @field #AI_A2G_DISPATCHER AI_A2G_DISPATCHER = { From e3121781d0d99b34a38628662d36c7194cb9bd6d Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Nov 2018 00:00:20 +0100 Subject: [PATCH 037/147] AIRBOSS v0.2.4 --- .../Moose/Functional/CarrierTrainer.lua | 320 ++++++++++++++---- Moose Development/Moose/Utilities/Utils.lua | 19 +- 2 files changed, 274 insertions(+), 65 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 296857418..06229a2bc 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -47,12 +47,18 @@ -- @field #AIRBOSS.Checkpoint Wake Right behind the carrier. -- @field #AIRBOSS.Checkpoint Groove In the groove checkpoint. -- @field #AIRBOSS.Checkpoint Trap Landing checkpoint. +-- @field #AIRBOSS.Checkpoint C3Descent4k Case III descent at 4000 ft/min right after leaving holding pattern. +-- @field #AIRBOSS.Checkpoint C3Descent2k Case III descent at 2000 ft/min at 5000 ft plattform. +-- @field #AIRBOSS.Checkpoint C3DirtyUp Case III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. +-- @field #AIRBOSS.Checkpoint C3BullsEye Case III intercept glideslope and follow ICLS "bullseye". -- @field #number rwyangle Angle of the runway wrt to carrier "nose". For the Stennis ~ -10 degrees. -- @field #number sterndist Distance in meters from carrier coordinate to the end of the deck. -- @field #number deckheight Height of the deck in meters. -- @field #number case Recovery case I, II or III. -- @field #table Qmarshal Queue of marshalling aircraft groups. -- @field #table Qpattern Queue of aircraft groups in the landing pattern. +-- @field #RESCUEHELO rescuehelo Rescue helo flying in close formation with the carrier. +-- @field #CARRIERTANKER tanker Refuelling tanker flying overhead with the carrier. -- @extends Core.Fsm#FSM --- Practice Carrier Landings @@ -95,12 +101,18 @@ AIRBOSS = { Wake = {}, Groove = {}, Trap = {}, + C3Descent4k = {}, + C3Descent2k = {}, + C3DirtyUp = {}, + C3BullsEye = {}, rwyangle = -9, sterndist =-100, deckheight = 22, case = 1, Qpattern = {}, Qmarshal = {}, + rescuehelo = nil, + tanker = nil, } --- Aircraft types. @@ -240,8 +252,9 @@ AIRBOSS.GroovePos={ --- Player data table holding all important parameters of each player. -- @type AIRBOSS.PlayerData --- @field Wrapper.Client#CLIENT client Client object of player. -- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string name Player name. +-- @field Wrapper.Client#CLIENT client Client object of player. -- @field Wrapper.Group#GROUP group Aircraft group the player is in. -- @field #string callsign Callsign of player. -- @field #string difficulty Difficulty level. @@ -293,7 +306,7 @@ AIRBOSS.MenuF10={} --- Carrier trainer class version. -- @field #string version -AIRBOSS.version="0.2.3" +AIRBOSS.version="0.2.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -329,16 +342,9 @@ function AIRBOSS:New(carriername, alias) -- Set carrier unit. self.carrier=UNIT:FindByName(carriername) - -- Carrier zones. - if self.carrier then - -- Zone 5 km astern and 100 m starboard of the carrier with radius of 2.5 km. - self.registerZone = ZONE_UNIT:New("registerZone", self.carrier, 2.5*1000, {dx = -5000, dy = 100, relative_to_unit=true}) - -- Zone 2 km astern and 100 m starboard of the carrier with a radius of 1 km. - self.startZone = ZONE_UNIT:New("startZone", self.carrier, 1.0*1000, {dx = -2000, dy = 100, relative_to_unit=true}) - -- Zone around the carrier with a radius of 30 km. - self.carrierZone = ZONE_UNIT:New("carrierZone", self.carrier, 10.0*1000) - else - -- Carrier unit does not exist error. + -- Check if carrier unit exists. + if self.carrier==nil then + -- Error message. 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(text) @@ -381,6 +387,18 @@ function AIRBOSS:New(carriername, alias) return nil end + -- Zone 5 km astern and 100 m starboard of the carrier with radius of 2.5 km. + self.registerZone = ZONE_UNIT:New("registerZone", self.carrier, 2.5*1000, {dx = -5000, dy = 100, relative_to_unit=true}) + + -- Zone 2 km astern and 100 m starboard of the carrier with a radius of 1 km. + self.startZone = ZONE_UNIT:New("startZone", self.carrier, 1.0*1000, {dx = -2000, dy = 100, relative_to_unit=true}) + + -- Zone around the carrier with a radius of 30 km. + self:SetCarrierControlledZone() + + -- Default recovery case. + self:SetRecoveryCase(3) + ----------------------- --- FSM Transitions --- ----------------------- @@ -420,6 +438,34 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Set carrier controlled zone. +-- This is a zone around the carrier which is constantly updated wrt the carrier position. +-- @param #AIRBOSS self +-- @param #number radius Radius of zone in nautical miles (NM). Default 50 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCarrierControlledZone(radius) + + radius=UTILS.NMToMeters(radius or 50) + + self.carrierZone=ZONE_UNIT:New("Carrier Controlled Zone", self.carrier, radius) + + return self +end + + + +--- Set recovery case pattern. +-- @param #AIRBOSS self +-- @param #number case Case of recovery. Either 1 or 3. +-- @return #AIRBOSS self +function AIRBOSS:SetRecoveryCase(case) + + self.case=case + + return self +end + + --- Set TACAN channel of carrier. -- @param #AIRBOSS self -- @param #number channel TACAN channel. @@ -566,11 +612,14 @@ function AIRBOSS:_CheckQueue() -- Collapse marshal stack. if nmarshal>0 and npattern<1 then - local flight=self.Qmarshal[1] --#AIRBOSS.Queueitem + -- First flight send to marshal stack. + local marshalflight=self.Qmarshal[1] --#AIRBOSS.Queueitem - local Tmarshal=timer.getTime()-flight.time - env.info(string.format("Marshal time of group %s = %d seconds", flight.groupname, Tmarshal)) + -- Time flight is marshalling. + local Tmarshal=timer.getTime()-marshalflight.time + env.info(string.format("Marshal time of group %s = %d seconds", marshalflight.groupname, Tmarshal)) + -- Time (last) flight has entered landing pattern. local Tpattern=999 if npattern>0 then local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.Queueitem @@ -578,8 +627,15 @@ function AIRBOSS:_CheckQueue() env.info(string.format("Pattern time of group %s = %d seconds", patternflight.groupname, Tpattern)) end + local TpatternMin=120 + if self.case==1 then + TpatternMin=45 + end + + local TmarshalMin=120 + -- Two minutes in pattern at leastand >45 sec interval between pattern flights. - if Tmarshal>120 and Tpattern>45 then + if Tmarshal>TmarshalMin and Tpattern>TpatternMin then self:_CollapseMarshalStack() end @@ -681,6 +737,13 @@ function AIRBOSS:_MarshalPlayer(group, stack) -- Add group to marshal stack. self:_AddMarshallGroup(group, nstacks+1) + --[[ + local playerData=self:_GetPlayerDataGroup(group) + if playerData then + self:_SendMessageToPlayer(message,duration,playerData,clear,sender,delay) + end + ]] + --TODO: playerData set end @@ -699,17 +762,15 @@ function AIRBOSS:_MarshalAI(group) local Carrier=self.carrier:GetCoordinate() -- Aircraft speed when flying the pattern. - local Speed=UTILS.KnotsToMps(272) + local Speed=UTILS.KnotsToMps(250) --- Create a DCS task to orbit at a certain altitude. - local function _taskorbit(coord, alt, speed, stopflag) - + local function _taskorbit(p1, alt, speed, stopflag, p2) local DCSTask={} DCSTask.id="ControlledTask" DCSTask.params={} - DCSTask.params.task=group:TaskOrbit(coord, alt, speed) - DCSTask.params.stopCondition={userFlag=groupname, userFlagValue=stopflag} - + DCSTask.params.task=group:TaskOrbit(p1, alt, speed, p2) + DCSTask.params.stopCondition={userFlag=groupname, userFlagValue=stopflag} return DCSTask end @@ -718,21 +779,35 @@ function AIRBOSS:_MarshalAI(group) -- Set up waypoints including collapsing the stack. local n=1 -- Waypoint counter. - for i=nstacks+1,1,-1 do - --env.info("FF i="..i) + for stack=nstacks+1,1,-1 do + + -- Altitude of first stack. Depends on recovery case. + local angels0 + local Dist + local p1=nil --Core.Point#COORDINATE + local p2=nil --Core.Point#COORDINATE + if self.case==1 then + angels0=2 + Dist=UTILS.NMToMeters(5) + p1=Carrier:Translate(Dist, 270) + else + angels0=6 + Dist=UTILS.NMToMeters((stack-1)*angels0+15) + p1=Carrier:Translate(Dist, self:_Radial()) + p2=Carrier:Translate(Dist+UTILS.NMToMeters(10), self:_Radial()) + end -- Pattern altitude. - local Altitude=UTILS.FeetToMeters((i-1)*1000+2000) + local Altitude=UTILS.FeetToMeters(((stack-1)+angels0)*1000) -- Orbit task. - local TaskOrbit=_taskorbit(Carrier, Altitude, Speed, i-1) + local TaskOrbit=_taskorbit(p1, Altitude, Speed, stack-1, p2) -- Waypoint description. - local text=string.format("Marshal @ %d ft, %d knots", UTILS.MetersToFeet(Altitude), UTILS.MpsToKnots(Speed)) - env.info(string.format("FF %s: %s stopFlag=%d", groupname, text, i-1)) + local text=string.format("Marshal @ alt=%d ft, dist=%.1f NM, speed=%d knots", UTILS.MetersToFeet(Altitude), UTILS.MetersToNM(Dist), UTILS.MpsToKnots(Speed)) -- Waypoint. - wp[n]=Carrier:SetAltitude(Altitude):WaypointAirTurningPoint(nil, Speed, {TaskOrbit}, text) + wp[n]=p1:SetAltitude(Altitude):WaypointAirTurningPoint(nil, Speed, {TaskOrbit}, text) -- Increase counter. n=n+1 @@ -741,8 +816,20 @@ function AIRBOSS:_MarshalAI(group) -- Landing waypoint. wp[#wp+1]=Carrier:WaypointAirLanding(Speed, self.airbase, nil, "Landing") + local angels0 + if self.case==1 then + angels0=2 + --Dist=UTILS.NMToMeters(5) + else + angels0=6 + --Dist=UTILS.NMToMeters(nstacks*angels0+15) + end + + -- Pattern altitude. + local Altitude=UTILS.FeetToMeters((nstacks+angels0)*1000) + -- Add group to marshal stack. - self:_AddMarshallGroup(group, nstacks+1) + self:_AddMarshallGroup(group, nstacks+1, Altitude) -- Reinit waypoints. group:WayPointInitialize(wp) @@ -755,7 +842,8 @@ end -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. -- @param #number flagvalue Initial user flag value. -function AIRBOSS:_AddMarshallGroup(group, flagvalue) +-- @param #number alt Altitude in feet. +function AIRBOSS:_AddMarshallGroup(group, flagvalue, alt) -- Flight group name local groupname=group:GetName() @@ -767,18 +855,18 @@ function AIRBOSS:_AddMarshallGroup(group, flagvalue) qitem.nunits=#group:GetUnits() qitem.fuel=group:GetFuelMin() qitem.time=timer.getTime() - qitem.stack=(flagvalue-1)*1000+2000 --TODO: Case III qitem.flag=USERFLAG:New(groupname) qitem.flag:Set(flagvalue) qitem.ai=not self:_IsHuman(group) - + qitem.stack=alt + -- Pressure. local hPa2inHg=0.0295299830714 local P=self.carrier:GetCoordinate():GetPressure()*hPa2inHg -- Marshal message. local text=string.format("XYZ, Case 1, BRC is 000, hold at %d. Expected Charlie Time XX.\n", qitem.stack) - text=text..string.format("Altimeter %.2f. Report see me.") + text=text..string.format("Altimeter %.2f. Report see me.", P) MESSAGE:New(text, 30):ToAll() -- Add to marshal queue. @@ -845,6 +933,36 @@ function AIRBOSS:_InQueue(queue, group) return false end +--- Get player data from unit object +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Unit in question. +-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. +function AIRBOSS:_GetPlayerDataUnit(unit) + if unit:IsAlive() then + local unitname=unit:GetName() + local playerunit,playername=self:_GetPlayerUnitAndName(unitname) + if playerunit and playername then + return self.players[playername] + end + end + return nil +end + + +--- Get player data from group object. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Group in question. +-- -- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. +function AIRBOSS:_GetPlayerDataGroup(group) + local units=group:GetUnits() + for _,unit in pairs(units) do + local playerdata=self:_GetPlayerDataUnit(unit) + if playerdata then + return playerdata + end + end + return nil +end --- Check current player status. -- @param #AIRBOSS self @@ -1083,35 +1201,45 @@ end -- @param #AIRBOSS self -- @param #string unitname Name of the player unit. -- @return #AIRBOSS.PlayerData Player data. -function AIRBOSS:_InitPlayer(unitname) +function AIRBOSS:_InitPlayer(unitname) - -- Player data. - local playerData={} --#AIRBOSS.PlayerData + -- Get player unit and name. + local playerunit, playername=self:_GetPlayerUnitAndName(unitname) - -- Player unit, client and callsign. - playerData.unit = UNIT:FindByName(unitname) - playerData.client = CLIENT:FindByName(unitname, nil, true) - playerData.callsign = playerData.unit:GetCallsign() - - -- Number of passes done by player. - playerData.passes=playerData.passes or 0 + if playerunit and playername then + + -- Player data. + local playerData={} --#AIRBOSS.PlayerData - -- LSO grades. - playerData.grades=playerData.grades or {} + -- Player unit, client and callsign. + playerData.unit = playerunit + playerData.name = playername + playerData.callsign = playerData.unit:GetCallsign() + playerData.client = CLIENT:FindByName(unitname, nil, true) + + -- Number of passes done by player. + playerData.passes=playerData.passes or 0 + + -- LSO grades. + playerData.grades=playerData.grades or {} + + -- Attitude monitor. + playerData.attitudemonitor=false + + -- Set difficulty level. + playerData.difficulty=playerData.difficulty or AIRBOSS.Difficulty.NORMAL + + -- Player is in the big zone around the carrier. + playerData.inbigzone=playerData.unit:IsInZone(self.carrierZone) - -- Attitude monitor. - playerData.attitudemonitor=false + -- Init stuff for this round. + playerData=self:_InitNewRound(playerData) + + -- Return player data table. + return playerData + end - -- Set difficulty level. - playerData.difficulty=playerData.difficulty or AIRBOSS.Difficulty.NORMAL - - -- Player is in the big zone around the carrier. - playerData.inbigzone=playerData.unit:IsInZone(self.carrierZone) - - -- Init stuff for this round. - playerData=self:_InitNewRound(playerData) - - return playerData + return nil end --- Initialize new approach for player by resetting parmeters to initial values. @@ -1178,7 +1306,7 @@ end function AIRBOSS:_Upwind(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z = self:_GetDistances(playerData.unit) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) -- Abort condition check. if self:_CheckAbort(X, Z, self.Upwind) then @@ -1214,7 +1342,7 @@ end function AIRBOSS:_Break(playerData, part) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z = self:_GetDistances(playerData.unit) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) -- Early or late break. local breakpoint = self.BreakEarly @@ -1890,6 +2018,70 @@ function AIRBOSS:_Lineup(playerData) return math.deg(lineup), UTILS.VecNorm(c) end +--- Get base recovery course (BRC) of carrier. +-- @param #AIRBOSS self +-- @param #boolean True If true, return true bearing. Otherwise (default) return magnetic bearing. +-- @return #number BRC in degrees. +function AIRBOSS:_BaseRecoveryCourse(True) + + -- Current true heading of carrier. + local hdg=self.carrier:GetHeading() + + -- Final (true) bearing. + local brc=hdg + + -- Magnetic bearing. + if True==false then + --TODO: Conversion to magnetic, i.e. include magnetic declination of current map. + end + + -- Adjust negative values. + if brc<0 then + brc=brc+360 + end + + return brc +end + + +--- Get final bearing (FB) of carrier. +-- By default, the routine returns the magnetic FB depending on the current map (Caucasus, NTTR, Normandy, Persion Gulf etc). +-- The true bearing can be obtained by setting the *True* parameter to true. +-- @param #AIRBOSS self +-- @param #boolean True If true, return true bearing. Otherwise (default) return magnetic bearing. +-- @return #number FB in degrees. +function AIRBOSS:_FinalBearing(True) + + -- Base Recovery Course of carrier. + local brc=self:_BaseRecoveryCourse(True) + + -- Final baring = BRC including angled deck. + local fb=brc+self.rwyangle + + -- Adjust negative values. + if fb<0 then + fb=fb+360 + end + + return fb +end + +--- Get radial, i.e. the final bearing FB-180 degrees. +-- @param #AIRBOSS self +-- @return #number Radial in degrees. +function AIRBOSS:_Radial() + + -- Get radial. + local radial=self:_FinalBearing()-180 + + -- Adjust for negative values. + if radial<0 then + radial=radial+360 + end + + return radial +end + --------- -- Bla functions @@ -2192,13 +2384,13 @@ end function AIRBOSS:_InitStennis() -- Carrier Parameters. - self.rwyangle = -10 + self.rwyangle = -9 self.sterndist =-150 self.deckheight = 22 self.wire1 =-100 - self.wire2 =-90 - self.wire3 =-80 - self.wire4 =-70 + self.wire2 = -90 + self.wire3 = -80 + self.wire4 = -70 --[[ q0=self.carrier:GetCoordinate():SetAltitude(25) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 2c7068a83..cc82f72e5 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -43,6 +43,19 @@ BIGSMOKEPRESET = { HugeSmoke=7, } +--- DCS map as returned by env.mission.theatre. +-- @type DCSMAP +-- @field #string Caucasus Caucasus map. +-- @field #string Normandy Normandy map. +-- @field #string NTTR Nevada Test and Training Range map. +-- @field #string PersionGulf Persian Gulf map. +DCSMAP = { + Caucasus="Caucasus", + NTTR="NTTR", + Normandy="Normandy", + PersianGulf="Persian Gulf" +} + --- Utilities static class. -- @type UTILS UTILS = { @@ -717,4 +730,8 @@ function UTILS.TACANToFrequency(TACANChannel, TACANMode) end - +--- Returns the DCS map/theatre as optained by env.mission.theatre. +-- @return #string DCS map string. +function UTILS.GetDCSMap() + return env.mission.theatre +end From 7b53a43c5c4f30fde7adae378fa9fde32f0bb16c Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 13 Nov 2018 16:24:08 +0100 Subject: [PATCH 038/147] AB v0.2.4 --- .../Moose/Functional/CarrierTrainer.lua | 84 ++++++++++++++++--- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 06229a2bc..965c0410b 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -312,6 +312,7 @@ AIRBOSS.version="0.2.4" -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Transmission via radio. -- DONE: Add scoring to radio menu. -- DONE: Optimized debrief. -- DONE: Add automatic grading. @@ -456,11 +457,11 @@ end --- Set recovery case pattern. -- @param #AIRBOSS self --- @param #number case Case of recovery. Either 1 or 3. +-- @param #number case Case of recovery. Either 1 or 3. Default 1. -- @return #AIRBOSS self function AIRBOSS:SetRecoveryCase(case) - self.case=case + self.case=case or 1 return self end @@ -468,12 +469,12 @@ end --- Set TACAN channel of carrier. -- @param #AIRBOSS self --- @param #number channel TACAN channel. --- @param #string mode TACAN mode, i.e. "X" or "Y". +-- @param #number channel TACAN channel. Default 74. +-- @param #string mode TACAN mode, i.e. "X" or "Y". Default "X". -- @return #AIRBOSS self function AIRBOSS:SetTACAN(channel, mode) - self.TACANchannel=channel + self.TACANchannel=channel or 74 self.TACANmode=mode or "X" return self @@ -481,11 +482,11 @@ end --- Set ICLS channel of carrier. -- @param #AIRBOSS self --- @param #number channel ICLS channel. +-- @param #number channel ICLS channel. Default 1. -- @return #AIRBOSS self function AIRBOSS:SetICLS(channel) - self.ICLSchannel=channel + self.ICLSchannel=channel or 1 return self end @@ -493,22 +494,22 @@ end --- Set LSO radio frequency. -- @param #AIRBOSS self --- @param #number freq Frequency in MHz. +-- @param #number freq Frequency in MHz. Default 264 MHz. -- @return #AIRBOSS self function AIRBOSS:SetLSOradio(freq) - self.LSOfreq=freq + self.LSOfreq=(freq or 264)*1000000 return self end --- Set carrier radio frequency. -- @param #AIRBOSS self --- @param #number freq Frequency in MHz. +-- @param #number freq Frequency in MHz. Default 305. -- @return #AIRBOSS self function AIRBOSS:SetCarrierradio(freq) - self.Carrierfreq=freq + self.Carrierfreq=(freq or 305)*1000000 return self end @@ -2239,8 +2240,13 @@ function AIRBOSS:_GetDistances(unit) 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 + + if phi<0 then + phi=phi+360 + end return dx,dz,rho,phi end @@ -2400,7 +2406,63 @@ function AIRBOSS:_InitStennis() q2=self.carrier:GetCoordinate():Translate(-68,0):SetAltitude(22) --4th wire ==> distance between wires 12 m q2:BigSmokeSmall(0.1)--:SmokeBlue() ]] + + -- 4k descent from holding pattern to 5k platform + self.C3Descent4k.name="4k Descent" + self.C3Descent4k.Xmin=-UTILS.NMToMeters(35) + self.C3Descent4k.Xmax=-UTILS.NMToMeters(20) + self.C3Descent4k.Zmin=-UTILS.NMToMeters(30) + self.C3Descent4k.Zmax= UTILS.NMToMeters(30) + self.C3Descent4k.LimitXmin=nil + self.C3Descent4k.LimitXmax=-UTILS.NMToMeters(20) --TODO: better rho dist. decrease descent 20 2000 ft/min at 5000 ft alt and user rad alt. + self.C3Descent4k.LimitZmin=nil + self.C3Descent4k.LimitZmax=nil + self.C3Descent4k.Altitude=nil --UTILS.FeetToMeters(5000) + self.C3Descent4k.AoA=nil + self.C3Descent4k.Distance=nil + + -- 2k descent from 5k platform to 1200 dirty up level flight. + self.C3Descent2k.name="2k Descent" + self.C3Descent2k.Xmin=-UTILS.NMToMeters(21) + self.C3Descent2k.Xmax=nil + self.C3Descent2k.Zmin=-UTILS.NMToMeters(30) + self.C3Descent2k.Zmax= UTILS.NMToMeters(30) + self.C3Descent2k.LimitXmin=nil + self.C3Descent2k.LimitXmax=-UTILS.NMToMeters(12) --TODO: better rho dist! now switch to dirty up level flight 12 NM. + self.C3Descent2k.LimitZmin=nil + self.C3Descent2k.LimitZmax=nil + self.C3Descent2k.Altitude=UTILS.FeetToMeters(5000) + self.C3Descent2k.AoA=nil + self.C3Descent2k.Distance=-UTILS.NMToMeters(20) + -- Level out at 1200 ft and dirty up. + self.C3DirtyUp.name="Dirty Up" + self.C3DirtyUp.Xmin=-UTILS.NMToMeters(13) + self.C3DirtyUp.Xmax=nil + self.C3DirtyUp.Zmin=-UTILS.NMToMeters(30) + self.C3DirtyUp.Zmax= UTILS.NMToMeters(30) + self.C3DirtyUp.LimitXmin=nil + self.C3DirtyUp.LimitXmax=-UTILS.NMToMeters(3) --TODO: better rho dist! Intercept glideslope and follow bullseye. + self.C3DirtyUp.LimitZmin=nil + self.C3DirtyUp.LimitZmax=nil + self.C3DirtyUp.Altitude=UTILS.FeetToMeters(1200) + self.C3DirtyUp.AoA=nil + self.C3DirtyUp.Distance=-UTILS.NMToMeters(12) + + -- Intercept glide slope and follow bullseye. + self.C3DirtyUp.name="Bullseye" + self.C3DirtyUp.Xmin=-UTILS.NMToMeters(4) + self.C3DirtyUp.Xmax=nil + self.C3DirtyUp.Zmin=-UTILS.NMToMeters(30) + self.C3DirtyUp.Zmax= UTILS.NMToMeters(30) + self.C3DirtyUp.LimitXmin=nil + self.C3DirtyUp.LimitXmax=-UTILS.NMToMeters(1) --TODO: better rho dist! Call the ball. + self.C3DirtyUp.LimitZmin=nil + self.C3DirtyUp.LimitZmax=nil + self.C3DirtyUp.Altitude=UTILS.FeetToMeters(1200) + self.C3DirtyUp.AoA=nil + self.C3DirtyUp.Distance=-UTILS.NMToMeters(3) + -- Upwind leg self.Upwind.name="Upwind" self.Upwind.Xmin=-4000 -- TODO Should be withing 4 km behind carrier. Why? From 84e9d225e91929d2d6a9a3ce7241caabeab189d6 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 13 Nov 2018 20:20:42 +0100 Subject: [PATCH 039/147] Fixing problem with squadron not found for CAS tasking. --- Moose Development/Moose/AI/AI_A2G_Dispatcher.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index d0c8a273e..1339e63f0 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -2637,8 +2637,7 @@ do -- AI_A2G_DISPATCHER if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then - local Squadron = self:GetSquadronFromDefender( Defender ) - local SquadronOverhead = self:GetSquadronOverhead( Squadron.SquadronName ) + local SquadronOverhead = self:GetSquadronOverhead( DefenderSquadronName ) local DefenderSize = Defender:GetInitialSize() if DefenderSize then From fa0535288232bb5dc19bc7b2fb0ff272aa442a06 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Nov 2018 23:39:29 +0100 Subject: [PATCH 040/147] AIRBOSS v0.2.5 --- .../Moose/Functional/CarrierTrainer.lua | 249 ++++++++++++++---- Moose Development/Moose/Utilities/Utils.lua | 4 +- 2 files changed, 205 insertions(+), 48 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 965c0410b..283359ed9 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -26,7 +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 #string alias Alias of the carrier. -- @field Wrapper.Airbase#AIRBASE airbase Carrier airbase object. -- @field Core.Radio#BEACON beacon Carrier beacon for TACAN and ICLS. -- @field #number TACANchannel TACAN channel. @@ -37,6 +37,7 @@ -- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. -- @field Core.Zone#ZONE_UNIT carrierZone 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 Core.Zone#ZONE_UNIT zoneHolding Zone where aircraft are holding before entering the landing pattern. -- @field #table players Table of players. -- @field #table menuadded Table of units where the F10 radio menu was added. -- @field #AIRBOSS.Checkpoint Upwind Upwind checkpoint. @@ -59,15 +60,16 @@ -- @field #table Qpattern Queue of aircraft groups in the landing pattern. -- @field #RESCUEHELO rescuehelo Rescue helo flying in close formation with the carrier. -- @field #CARRIERTANKER tanker Refuelling tanker flying overhead with the carrier. +-- @field #table recoverytime Time interval where aircraft are recovered. -- @extends Core.Fsm#FSM --- Practice Carrier Landings -- -- === -- --- ![Banner Image](..\Presentations\AIRBOSS\CarrierTrainer_Main.png) +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Main.png) -- --- # The Trainer Concept +-- # The AIRBOSS Concept -- -- bla bla -- @@ -91,6 +93,7 @@ AIRBOSS = { registerZone = nil, startZone = nil, carrierZone = nil, + zoneHolding = nil, players = {}, menuadded = {}, Upwind = {}, @@ -113,6 +116,7 @@ AIRBOSS = { Qmarshal = {}, rescuehelo = nil, tanker = nil, + recoverytime = {}, } --- Aircraft types. @@ -216,6 +220,11 @@ AIRBOSS.Difficulty={ HARD="TOPGUN Graduate", } +--- Recovery time. +-- @type AIRBOSS.Recovery +-- @field #number START Start of recovery. +-- @field #number STOP End of recovery. + --- Groove position. -- @type AIRBOSS.GroovePos -- @field #string X0 Entering the groove. @@ -279,6 +288,10 @@ AIRBOSS.GroovePos={ -- @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 Rmin Minimum allowed range to carrier. +-- @field #number Rmax Maximum allowed range to carrier. +-- @field #number Amin Minimum allowed angle to carrier. +-- @field #number Amax Maximum allowed angle 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 Z Event --> To State self:AddTransition("Stopped", "Start", "Running") - self:AddTransition("Running", "Status", "Running") + self:AddTransition("Running", "Recover", "Recovering") -- Recover aircraft. + self:AddTransition("*", "Status", "*") self:AddTransition("*", "Stop", "Stopped") - --- Triggers the FSM event "Start" that starts the carrier trainer. Initializes parameters and starts event handlers. + --- Triggers the FSM event "Start" that starts the airboss. Initializes parameters and starts event handlers. -- @function [parent=#AIRBOSS] Start -- @param #AIRBOSS self - --- Triggers the FSM event "Start" after a delay that starts the carrier trainer. Initializes parameters and starts event handlers. + --- Triggers the FSM event "Start" that starts the airboss after a delay. Initializes parameters and starts event handlers. -- @function [parent=#AIRBOSS] __Start -- @param #AIRBOSS self -- @param #number delay Delay in seconds. - --- Triggers the FSM event "Stop" that stops the carrier trainer. Event handlers are stopped. + + --- Triggers the FSM event "Recover" that starts the recovering of aircraft. Marshalling aircraft are send to the landing pattern. + -- @function [parent=#AIRBOSS] Recover + -- @param #AIRBOSS self + + --- Triggers the FSM event "Recover" that starts the recovering of aircraft after a delay. Marshalling aircraft are send to the landing pattern. + -- @function [parent=#AIRBOSS] __Start + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. -- @function [parent=#AIRBOSS] Stop -- @param #AIRBOSS self - --- Triggers the FSM event "Stop" that stops the carrier trainer after a delay. Event handlers are stopped. + --- Triggers the FSM event "Stop" that stops the airboss after a delay. Event handlers are stopped. -- @function [parent=#AIRBOSS] __Stop -- @param #AIRBOSS self -- @param #number delay Delay in seconds. @@ -466,6 +491,24 @@ function AIRBOSS:SetRecoveryCase(case) return self end +--- Add recovery time slot. +-- @param #AIRBOSS self +-- @param #string starttime Start time, e.g. "8:00" for eight o'clock. +-- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. +-- @return #AIRBOSS self +function AIRBOSS:AddRecoveryTime(starttime, stoptime) + + local Tstart=UTILS.ClockToSeconds(starttime) + local Tstop=UTILS.ClockToSeconds(stoptime) + + local rtime={} --#AIRBOSS.Recovery + rtime.START=Tstart + rtime.STOP=Tstop + + table.insert(self.recoverytime, rtime) + return self +end + --- Set TACAN channel of carrier. -- @param #AIRBOSS self @@ -514,6 +557,21 @@ function AIRBOSS:SetCarrierradio(freq) return self end + +--- Check if carrier is recovering aircraft. +-- @param #AIRBOSS self +-- @return #boolean If true, time slot for recovery is open. +function AIRBOSS:IsRecovering() + return self:is("Recovering") +end + +--- Check if carrier is operating. +-- @param #AIRBOSS self +-- @return #boolean If true, helo is operating. +function AIRBOSS:IsRunning() + return self:is("Running") +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM states ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -560,6 +618,15 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Get current time. local time=timer.getTime() + -- Check if we go into recovery mode. + local startrecovery=self:_CheckRecoveryTimes() + if startrecovery==true then + self:Recover() + end + + local text=string.format("AIRBOSS %s: Status %s.", self.alias, self:GetState()) + self:I(text) + -- Update marshal and pattern queue every 30 seconds. if time-self.Tqueue>30 then @@ -576,10 +643,54 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Check player status. self:_CheckPlayerStatus() - -- Call status again in 0.25 seconds. + -- Call status again in one second. self:__Status(-1) end +--- Check if recovery times. +-- @param #AIRBOSS self +-- @return #boolean IF true, start recovery. +function AIRBOSS:_CheckRecoveryTimes() + + local abstime=timer.getAbsTime() + + if #self.recoverytime==0 then + + -- If no recovery times have been specified, we assume any time is okay. + self:I("FF Start recovery. No recovery time set!") + + return true + else + + local recovery=false + for _,_rtime in pairs(self.recoverytime) do + local rtime=_rtime --#AIRBOSS.Recovery + if abstime>=rtime.START and abstime<=rtime.STOP then + if not self:IsRecovering() then + self:I("FF Start recovery.") + return true + else + return nil + end + end + end + + return false + end + +end + +--- On before "Recover" event. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean If true, recovery transition is allowed. +function AIRBOSS:onbeforeRecover(From, Event, To) + return true +end + + --- On after Stop event. Unhandle events and stop status updates. -- @param #AIRBOSS self -- @param #string From From state. @@ -636,7 +747,7 @@ function AIRBOSS:_CheckQueue() local TmarshalMin=120 -- Two minutes in pattern at leastand >45 sec interval between pattern flights. - if Tmarshal>TmarshalMin and Tpattern>TpatternMin then + if self:IsRecovering() and Tmarshal>TmarshalMin and Tpattern>TpatternMin then self:_CollapseMarshalStack() end @@ -788,6 +899,7 @@ function AIRBOSS:_MarshalAI(group) local p1=nil --Core.Point#COORDINATE local p2=nil --Core.Point#COORDINATE if self.case==1 then + -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next aircraft. angels0=2 Dist=UTILS.NMToMeters(5) p1=Carrier:Translate(Dist, 270) @@ -820,10 +932,8 @@ function AIRBOSS:_MarshalAI(group) local angels0 if self.case==1 then angels0=2 - --Dist=UTILS.NMToMeters(5) else angels0=6 - --Dist=UTILS.NMToMeters(nstacks*angels0+15) end -- Pattern altitude. @@ -890,6 +1000,11 @@ function AIRBOSS:_CollapseMarshalStack() -- TODO: better message. MESSAGE:New(string.format("Marshal, %s, you are cleared for Case I recovery pattern!", flight.groupname), 15):ToAll() + if flight.ai==false then + local playerData=self:_GetPlayerDataGroup(flight.group) + playerData.step=0 + end + -- Time stamp. flight.time=timer.getTime() @@ -953,7 +1068,7 @@ end --- Get player data from group object. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Group in question. --- -- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. +-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. function AIRBOSS:_GetPlayerDataGroup(group) local units=group:GetUnits() for _,unit in pairs(units) do @@ -971,12 +1086,12 @@ function AIRBOSS:_CheckPlayerStatus() -- Loop over all players. for _playerName,_playerData in pairs(self.players) do - local playerData = _playerData --#AIRBOSS.PlayerData + local playerData=_playerData --#AIRBOSS.PlayerData if playerData then -- Player unit. - local unit = playerData.unit + local unit=playerData.unit if unit:IsAlive() then @@ -985,6 +1100,7 @@ function AIRBOSS:_CheckPlayerStatus() self:_DetailedPlayerStatus(playerData) end + -- Check if player is in carrier controlled zone. if unit:IsInZone(self.carrierZone) then -- Check if player was previously not inside the zone. @@ -1014,38 +1130,58 @@ function AIRBOSS:_CheckPlayerStatus() self.groovedebug=false end elseif playerData.step==1 then + -- Entering the pattern. self:_Start(playerData) + elseif playerData.step==2 then + -- Upwind leg. self:_Upwind(playerData) + elseif playerData.step==3 then + -- Early break. self:_Break(playerData, "early") + elseif playerData.step==4 then + -- Late break. self:_Break(playerData, "late") + elseif playerData.step==5 then + -- Abeam position. self:_Abeam(playerData) + elseif playerData.step==6 then + -- Check long down wind leg. self:_CheckForLongDownwind(playerData) -- At the ninety. self:_Ninety(playerData) + elseif playerData.step==7 then + -- In the wake. self:_Wake(playerData) + elseif playerData.step==90 then + -- Entering the groove. self:_Groove(playerData) + elseif playerData.step>=91 and playerData.step<=99 then + -- In the groove. self:_CallTheBall(playerData) + elseif playerData.step==999 then + -- Debriefing. SCHEDULER:New(nil, self._Debrief, {self, playerData}, 10) - playerData.step=-1 + playerData.step=-999 + end else @@ -1065,7 +1201,7 @@ end -- EVENT functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Carrier trainer event handler for event birth. +--- Airboss event handler for event birth. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData function AIRBOSS:OnEventBirth(EventData) @@ -1098,7 +1234,7 @@ function AIRBOSS:OnEventBirth(EventData) end end if rightaircraft==false then - self:E(string.format("Player aircraft %s not supported of CARRIERTRAINTER.", aircraft)) + self:E(string.format("Player aircraft %s not supported by AIRBOSS class.", aircraft)) return end @@ -1158,7 +1294,8 @@ function AIRBOSS:OnEventLand(EventData) env.info("FF landed") playerData.landed=true - playerData.step=-1 + -- Unkonwn step. + playerData.step=-999 --TODO: maybe check that we actually landed on the right carrier. @@ -1173,6 +1310,24 @@ function AIRBOSS:OnEventLand(EventData) end +--- Airboss event handler for event crash. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventCrash(EventData) + self:F3({eventland = EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:I(self.lid.."CRASH: unit = "..tostring(EventData.IniUnitName)) + self:I(self.lid.."CRASH: group = "..tostring(EventData.IniGroupName)) + self:I(self.lid.."CARSH: player = "..tostring(_playername)) + + if _unit and _playername then + + end +end + --- Airboss event handler for event land. -- @param #AIRBOSS self -- @param #table queue The queue from which the group will be removed. @@ -1195,7 +1350,7 @@ function AIRBOSS:_RemoveQueue(queue, group) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- CARRIER TRAINING functions +-- AIRBOSS functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Initialize player data. @@ -1278,7 +1433,7 @@ function AIRBOSS:_NewRound(playerData) end end ---- Start pattern when player enters the start zone. +--- Start pattern when player enters the initial zone. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Start(playerData) @@ -1370,10 +1525,10 @@ function AIRBOSS:_Break(playerData, part) self:_SendMessageToPlayer(hint, 10, playerData) -- Debrief - if part=="late" then - self:_AddToSummary(playerData, "Late Break", debrief) + if part=="early" then + self:_AddToSummary(playerData, "Early Break", debrief) else - self:_AddToSummary(playerData, "Early Break", debrief) + self:_AddToSummary(playerData, "Late Break", debrief) end -- Next step: late break or abeam. @@ -3043,34 +3198,34 @@ function AIRBOSS:_AddF10Commands(_unitName) -- Enable switch so we don't do this twice. self.menuadded[_gid] = true - -- Main F10 menu: F10/Carrier Trainer// + -- Main F10 menu: F10/Airboss// if AIRBOSS.MenuF10[_gid] == nil then - AIRBOSS.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "Carrier Trainer") + AIRBOSS.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "Airboss") end -- Player Data. local playerData=self.players[playername] - -- F10/Carrier Trainer/ + -- F10/Airboss/ local _trainPath = missionCommands.addSubMenuForGroup(_gid, self.alias, AIRBOSS.MenuF10[_gid]) - -- F10/Carrier Trainer//Results + -- F10/Airboss//Results local _statsPath = missionCommands.addSubMenuForGroup(_gid, "LSO Grades", _trainPath) - -- F10/Carrier Trainer//My Settings/Difficulty + -- F10/Airboss//My Settings/Difficulty local _difficulPath = missionCommands.addSubMenuForGroup(_gid, "Difficulty", _trainPath) - -- F10/Carrier Trainer//Results/ + -- F10/Airboss//Results/ missionCommands.addCommandForGroup(_gid, "Greenie Board", _statsPath, self._DisplayScoreBoard, self, _unitName) missionCommands.addCommandForGroup(_gid, "My Grades", _statsPath, self._DisplayPlayerGrades, self, _unitName) --missionCommands.addCommandForGroup(_gid, "(Clear ALL Results)", _statsPath, self._ResetRangeStats, self, _unitName) - -- F10/Carrier Trainer//Difficulty + -- F10/Airboss//Difficulty missionCommands.addCommandForGroup(_gid, "Flight Student", _difficulPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) missionCommands.addCommandForGroup(_gid, "Naval Aviator", _difficulPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _difficulPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) - -- F10/Carrier Trainer// + -- F10/Airboss// missionCommands.addCommandForGroup(_gid, "Carrier Info", _trainPath, self._DisplayCarrierInfo, self, _unitName) missionCommands.addCommandForGroup(_gid, "Weather Report", _trainPath, self._DisplayCarrierWeather, self, _unitName) missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _trainPath, self._AttitudeMonitor, self, playername) @@ -3246,15 +3401,17 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) -- Tacan/ICLS. local tacan="unknown" local icls="unknown" - if self.TACAN~=nil then - tacan=tostring(self.TACAN) + if self.TACANchannel~=nil then + tacan=string.format("%d%s", self.TACANchannel, self.TACANmode) end if self.ICLSchannel~=nil then - icls=tostring(self.ICLS) + icls=string.format("%d", self.ICLSchannel) end -- Message text - text=text..string.format("BRC %d°\n", carrierheading) + text=text..string.format("Case %d Recovery\n", self.case) + text=text..string.format("BRC %d°\n", self:_BaseRecoveryCourse()) + text=text..string.format("FB %d°\n", self:_FinalBearing()) text=text..string.format("Speed %d kts\n", carrierspeed) text=text..string.format("TACAN Channel %s\n", tacan) text=text..string.format("ICLS Channel %s", icls) @@ -3444,11 +3601,11 @@ function RESCUEHELO:New(carrierunit, helogroupname) self:AddTransition("Running", "Stop", "Stopped") - --- Triggers the FSM event "Start" that starts the carrier trainer. Initializes parameters and starts event handlers. + --- Triggers the FSM event "Start" that starts the rescue helo. Initializes parameters and starts event handlers. -- @function [parent=#RESCUEHELO] Start -- @param #RESCUEHELO self - --- Triggers the FSM event "Start" after a delay that starts the carrier trainer. Initializes parameters and starts event handlers. + --- Triggers the FSM event "Start" that starts the rescue helo after a delay. Initializes parameters and starts event handlers. -- @function [parent=#RESCUEHELO] __Start -- @param #RESCUEHELO self -- @param #number delay Delay in seconds. @@ -3873,11 +4030,11 @@ function CARRIERTANKER:New(carrierunit, tankergroupname) self:AddTransition("Running", "Stop", "Stopped") - --- Triggers the FSM event "Start" that starts the carrier trainer. Initializes parameters and starts event handlers. + --- Triggers the FSM event "Start" that starts the carrier tanker. Initializes parameters and starts event handlers. -- @function [parent=#CARRIERTANKER] Start -- @param #CARRIERTANKER self - --- Triggers the FSM event "Start" after a delay that starts the carrier trainer. Initializes parameters and starts event handlers. + --- Triggers the FSM event "Start" that starts the carrier tanker after a delay. Initializes parameters and starts event handlers. -- @function [parent=#CARRIERTANKER] __Start -- @param #CARRIERTANKER self -- @param #number delay Delay in seconds. @@ -3891,11 +4048,11 @@ function CARRIERTANKER:New(carrierunit, tankergroupname) -- @param #CARRIERTANKER self -- @param #number delay Delay in seconds. - --- Triggers the FSM event "Stop" that stops the carrier trainer. Event handlers are stopped. + --- Triggers the FSM event "Stop" that stops the carrier tanker. Event handlers are stopped. -- @function [parent=#CARRIERTANKER] Stop -- @param #CARRIERTANKER self - --- Triggers the FSM event "Stop" that stops the carrier trainer after a delay. Event handlers are stopped. + --- Triggers the FSM event "Stop" that stops the carrier tanker after a delay. Event handlers are stopped. -- @function [parent=#CARRIERTANKER] __Stop -- @param #CARRIERTANKER self -- @param #number delay Delay in seconds. diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index cc82f72e5..c8c61c06d 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -552,7 +552,7 @@ end --- Convert clock time from hours, minutes and seconds to seconds. -- @param #string clock String of clock time. E.g., "06:12:35" or "5:1:30+1". Format is (H)H:(M)M:((S)S)(+D) H=Hours, M=Minutes, S=Seconds, D=Days. --- @param #number Seconds. Corresponds to what you cet from timer.getAbsTime() function. +-- @return #number Seconds. Corresponds to what you cet from timer.getAbsTime() function. function UTILS.ClockToSeconds(clock) -- Nil check. @@ -564,7 +564,7 @@ function UTILS.ClockToSeconds(clock) local seconds=0 -- Split additional days. - local dsplit=UTILS.split(clock, "+") + local dsplit=UTILS.Split(clock, "+") -- Convert days to seconds. if #dsplit>1 then From 3bc2baaf9da7823f2be41aca7382dc424f0de2b7 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 14 Nov 2018 16:11:54 +0100 Subject: [PATCH 041/147] AIRBOSS v0.2.5w --- Moose Development/Moose/Core/Radio.lua | 47 +- .../Moose/Functional/CarrierTrainer.lua | 651 ++++++++++++------ 2 files changed, 478 insertions(+), 220 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 90bfa23ef..d46b95310 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -290,16 +290,28 @@ end function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop) self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) + -- Set file name. self:SetFileName(FileName) - local Duration = 5 - if SubtitleDuration then Duration = SubtitleDuration end - -- SubtitleDuration argument was missing, adding it - if Subtitle then self:SetSubtitle(Subtitle, Duration) end - -- self:SetSubtitleDuration is non existent, removing faulty line - -- if SubtitleDuration then self:SetSubtitleDuration(SubtitleDuration) end - if Frequency then self:SetFrequency(Frequency) end - if Modulation then self:SetModulation(Modulation) end - if Loop then self:SetLoop(Loop) end + + -- Set frequency. + if Frequency then + self:SetFrequency(Frequency) + end + + -- Set modulation AM/FM. + if Modulation then + self:SetModulation(Modulation) + end + + -- Set subtitle. + if Subtitle then + self:SetSubtitle(Subtitle, SubtitleDuration or 0) + end + + -- Set Looping. + if Loop then + self:SetLoop(Loop) + end return self end @@ -313,19 +325,26 @@ end -- * If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored. -- * If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration are ignored -- @param #RADIO self +-- @param #string filename (Optinal) Sound file name. Default self.FileName. +-- @param #string subtitle (Optional) Subtitle. Default self.Subtitle. +-- @param #number subtitleduraction (Optional) Subtitle duraction. Default self.SubtitleDuration. -- @return #RADIO self -function RADIO:Broadcast() +function RADIO:Broadcast(filename, subtitle, subtitleduration) self:F() + filename=filename or self.FileName + subtitle=subtitle or self.Subtitle + subtitleduration=subtitleduration or self.SubtitleDuration + -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then self:T2("Broadcasting from a UNIT or a GROUP") self.Positionable:SetCommand({ id = "TransmitMessage", params = { - file = self.FileName, - duration = self.SubtitleDuration, - subtitle = self.Subtitle, + file = filename, + duration = subtitleduration, + subtitle = subtitle, loop = self.Loop, } }) @@ -338,6 +357,8 @@ function RADIO:Broadcast() return self end + + --- Stops a transmission -- This function is especially usefull to stop the broadcast of looped transmissions -- @param #RADIO self diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 283359ed9..3f6975264 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -4,9 +4,16 @@ -- -- Features: -- --- * CASE I recovery. --- * Performance evaluation. --- * Feedback about performance during flight. +-- * CASE I and III recovery. +-- * Supports human and AI pilots. +-- * Automatic LSO grading. +-- * Different skill level supporting tipps during for students or complete zip lip for pros. +-- * Rescue helo option. +-- * Overhead refuelling tanker option. +-- * Voice overs for LSO and Airobss calls. Can easily customized by users. +-- * Automatic TACAN and ICLS channel setting. +-- * Different radio channels for LSO and airboss calls. +-- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels, pilot grades). -- -- 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. @@ -26,6 +33,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 #AIRBOSS.CarrierParameters carrierparam Carrier specifc parameters. -- @field #string alias Alias of the carrier. -- @field Wrapper.Airbase#AIRBASE airbase Carrier airbase object. -- @field Core.Radio#BEACON beacon Carrier beacon for TACAN and ICLS. @@ -34,6 +42,7 @@ -- @field #number ICLSchannel ICLS channel. -- @field Core.Radio#RADIO LSOradio Radio for LSO calls. -- @field Core.Radio#RADIO Carrierradio Radio for carrier calls. +-- @field #AIRBOSS.RadioCalls radiocall LSO and Airboss call sound files and texts. -- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. -- @field Core.Zone#ZONE_UNIT carrierZone Large zone around the carrier to welcome players. -- @field Core.Zone#ZONE_UNIT registerZone Zone behind the carrier to register for a new approach. @@ -80,6 +89,7 @@ AIRBOSS = { Debug = true, carrier = nil, carriertype = nil, + carrierparam = nil, alias = nil, airbase = nil, beacon = nil, @@ -90,6 +100,7 @@ AIRBOSS = { LSOfreq = nil, Carrierradio = nil, Carrierfreq = nil, + radiocall = {}, registerZone = nil, startZone = nil, carrierZone = nil, @@ -108,9 +119,7 @@ AIRBOSS = { C3Descent2k = {}, C3DirtyUp = {}, C3BullsEye = {}, - rwyangle = -9, - sterndist =-100, - deckheight = 22, + radiocall = nil, case = 1, Qpattern = {}, Qmarshal = {}, @@ -141,74 +150,126 @@ AIRBOSS.CarrierType={ KUZNETSOV="KUZNECOW" } +--- Carrier Parameters. +-- @type AIRBOSS.CarrierParameter +-- @field #number rwyangle Runway angle in degrees. for carriers with angled deck. For USS Stennis -9 degrees. +-- @field #number sterndist Distance in meters from carrier position to stern of carrier. For USS Stennis -150 meters. +-- @field #number deckheight Height of deck in meters. For USS Stennis ~22 meters. +-- @field #number wire1 Distance in meters from carrier position to first wire. +-- @field #number wire2 Distance in meters from carrier position to second wire. +-- @field #number wire3 Distance in meters from carrier position to third wire. +-- @field #number wire4 Distance in meters from carrier position to fourth wire. + --- Pattern steps. -- @type AIRBOSS.PatternStep AIRBOSS.PatternStep={ + UNDEFINED="Undefined", UNREGISTERED="Unregistered", - PATTERNENTRY="Pattern Entry", + HOLDING="Holding", + DESCENT4K="Descent 4000 ft/min", + DESCENT2K="Descent 2000 ft/min", + DIRTYUP="Leven and Dirty Up", + BULLSEYE="Follow Bullseye", + INITIAL="Initial", + UPWIND="Upwind", EARLYBREAK="Early Break", LATEBREAK="Late Break", ABEAM="Abeam", NINETY="Ninety", WAKE="Wake", - GROOVE_X0="Groove Entry", + FINAL="On Final", GROOVE_XX="Groove X", GROOVE_RB="Groove Roger Ball", GROOVE_IM="Groove In the Middle", GROOVE_IC="Groove In Close", GROOVE_AR="Groove At the Ramp", GROOVE_IW="Groove In the Wires", + DEBRIEF="Debrief", } ---- LSO calls. --- @type AIRBOSS.LSOcall --- @field Core.UserSound#USERSOUND RIGHTFORLINEUPL "Right for line up!" call (loud). --- @field Core.UserSound#USERSOUND RIGHTFORLINEUPS "Right for line up." call. --- @field #string RIGHTFORLINEUPT "Right for line up" text. --- @field Core.UserSound#USERSOUND COMELEFTL "Come left!" call (loud). --- @field Core.UserSound#USERSOUND COMELEFTS "Come left." call. --- @field #string COMELEFTT "Come left" text. --- @field Core.UserSound#USERSOUND HIGHL "You're high!" call (loud). --- @field Core.UserSound#USERSOUND HIGHS "You're high." call. --- @field #string HIGHT "You're high" text. --- @field Core.UserSound#USERSOUND POWERL "Power!" call (loud). --- @field Core.UserSound#USERSOUND POWERS "Power." call. --- @field #string POWERT "Power" text. --- @field Core.UserSound#USERSOUND CALLTHEBALL "Call the ball." call. --- @field #string CALLTHEBALLT "Call the ball." text. --- @field Core.UserSound#USERSOUND ROGERBALL "Roger, ball." call. --- @field #string ROGERBALLT "Roger, ball." text. --- @field Core.UserSound#USERSOUND WAVEOFF "Wave off!" call. --- @field #string WAVEOFFT "Wave off!" text. --- @field Core.UserSound#USERSOUND BOLTER "Bolter, bolter!" call. --- @field #string BOLTERT "Bolter, bolter!" text. --- @field Core.UserSound#USERSOUND LONGGROOVE "You're long in the groove. Depart and re-enter." call. --- @field #string LONGGROOVET "You're long in the groove. Depart and re-enter." text. -AIRBOSS.LSOcall={ - RIGHTFORLINEUPL=USERSOUND:New("LSO - RightLineUp(L).ogg"), - RIGHTFORLINEUPS=USERSOUND:New("LSO - RightLineUp(S).ogg"), - RIGHTFORLINEUPT="Right for line up", - COMELEFTL=USERSOUND:New("LSO - ComeLeft(L).ogg"), - COMELEFTS=USERSOUND:New("LSO - ComeLeft(S).ogg"), - COMELEFTT="Come left", - HIGHL=USERSOUND:New("LSO - High(L).ogg"), - HIGHS=USERSOUND:New("LSO - High(S).ogg"), - HIGHT="You're high", - POWERL=USERSOUND:New("LSO - Power(L).ogg"), - POWERS=USERSOUND:New("LSO - Power(S).ogg"), - POWERT="Power", - CALLTHEBALL=USERSOUND:New("LSO - Call the Ball.ogg"), - CALLTHEBALLT="Call the ball.", - ROGERBALL=USERSOUND:New("LSO - Roger.ogg"), - ROGERBALLT="Roger ball!", - WAVEOFF=USERSOUND:New("LSO - WaveOff.ogg"), - WAVEOFFT="Wave off!", - BOLTER=USERSOUND:New("LSO - Bolter.ogg"), - BOLTERT="Bolter, Bolter!", - LONGGROOVE=USERSOUND:New("LSO - Long in Groove.ogg"), - LONGGROOVET="You're long in the groove. Depart and re-enter.", +--- Radio sound file and subtitle. +-- @type AIRBOSS.RadioSound +-- @field #string normal Sound file normal. +-- @field #string loud Sound file loud. +-- @field #string subtitle Subtitle displayed during transmission. +-- @field #number duration Duration in seconds the subtitle is displayed. + +--- LSO and Airboss radio calls. +-- @type AIRBOSS.RadioCalls +-- @field #AIRBOSS.RadioSound RIGHTFORLINEUP "Right for line up!" call. +-- @field #AIRBOSS.RadioSound COMELEFT "Come left!" call. +-- @field #AIRBOSS.RadioSound HIGH "You're high!" call. +-- @field #AIRBOSS.RadioSound POWER Sound file "Power!" call. +-- @field #AIRBOSS.RadioSound SLOW Sound file "You're slow!" call. +-- @field #AIRBOSS.RadioSound FAST Sound file "You're fast!" call. +-- @field #AIRBOSS.RadioSound CALLTHEBALL Sound file "Call the ball." call. +-- @field #AIRBOSS.RadioSound ROGERBALL "Roger, ball." call. +-- @field #AIRBOSS.RadioSound WAVEOFF "Wave off!" call. +-- @field #AIRBOSS.RadioSound BOLTER "Bolter, bolter!" call. +-- @field #AIRBOSS.RadioSound LONGINGROOVE "You're long in the groove. Depart and re-enter." call. + +--- Default radio call sound files. +-- @type AIRBOSS.Soundfile +-- @field #AIRBOSS.RadioSound RIGHTFORLINEUP +-- @field #AIRBOSS.RadioSound COMELEFT +-- @field #AIRBOSS.RadioSound +-- @field #AIRBOSS.RadioSound +-- @field #AIRBOSS.RadioSound +-- @field #AIRBOSS.RadioSound +-- @field #AIRBOSS.RadioSound +AIRBOSS.Soundfile={ + RIGHTFORLINEUP={ + normal="LSO - RightLineUp(L).ogg", + loud="LSO - RightLineUp(S).ogg", + subtitle="Right for line up.", + duration=3, + }, + COMELEFT={ + normal="LSO - ComeLeft(S).ogg", + loud="LSO - ComeLeft(L).ogg", + subtitle="Come left.", + duration=3, + }, + HIGH={ + normal="LSO - High(S).ogg", + loud="LSO - High(L).ogg", + subtitle="You're high.", + duration=3, + }, + POWER={ + normal="LSO - Power(S).ogg", + loud="LSO - Power(L).ogg", + subtitle="Power.", + duration=3, + }, + CALLTHEBALL={ + normal="LSO - Call the Ball.ogg", + subtitle="Call the ball.", + duration=3, + }, + ROGERBALL={ + normal="LSO - Roger.ogg", + subtitle="Roger ball!", + duration=3, + }, + WAVEOFF={ + normal="LSO - WaveOff.ogg", + subtitle="Wave off!", + duration=3, + }, + BOLTER={ + normal="LSO - Bolter.ogg", + subtitle="Bolter, Bolter!", + duration=3, + }, + LONGINGROOVE={ + normal="LSO - Long in Groove.ogg", + subtitle="You're long in the groove. Depart and re-enter.", + duration=3, + } } + --- Difficulty level. -- @type AIRBOSS.Difficulty -- @field #string EASY Easy difficulty: error margin 10 for high score and 20 for low score. No score for deviation >20. @@ -319,16 +380,14 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.2.5" +AIRBOSS.version="0.2.5w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Handle crash event. Delete ac from queue, send rescue helo, stop carrier? -- TODO: Transmission via radio. --- DONE: Add scoring to radio menu. --- DONE: Optimized debrief. --- DONE: Add automatic grading. -- TODO: Get board numbers. -- TODO: Get fuel state in pounds. -- TODO: Add user functions. @@ -337,6 +396,9 @@ AIRBOSS.version="0.2.5" -- TODO: CASE II. -- TODO: CASE III. -- TODO: Foul deck check. +-- DONE: Add scoring to radio menu. +-- DONE: Optimized debrief. +-- DONE: Add automatic grading. -- DONE: Fix radio menu. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -380,8 +442,10 @@ function AIRBOSS:New(carriername, alias) -- Create carrier beacon. self.beacon=BEACON:New(self.carrier) - -- Set up airboss and LSO radios + -- Set up Airboss radio. self.Carrierradio=RADIO:New(self.carrier) + + -- Set up LSO radio. self.LSOradio=RADIO:New(self.carrier) -- Init carrier parameters. @@ -541,7 +605,7 @@ end -- @return #AIRBOSS self function AIRBOSS:SetLSOradio(freq) - self.LSOfreq=(freq or 264)*1000000 + self.LSOfreq=freq or 264 return self end @@ -552,7 +616,7 @@ end -- @return #AIRBOSS self function AIRBOSS:SetCarrierradio(freq) - self.Carrierfreq=(freq or 305)*1000000 + self.Carrierfreq=freq or 305 return self end @@ -658,21 +722,31 @@ function AIRBOSS:_CheckRecoveryTimes() -- If no recovery times have been specified, we assume any time is okay. self:I("FF Start recovery. No recovery time set!") - - return true + if not self:IsRecovering() then + return true + else + return nil + end + else local recovery=false + for _,_rtime in pairs(self.recoverytime) do local rtime=_rtime --#AIRBOSS.Recovery + if abstime>=rtime.START and abstime<=rtime.STOP then + if not self:IsRecovering() then self:I("FF Start recovery.") return true else + -- Nothing to do. Return nil. return nil end - end + + end + end return false @@ -837,8 +911,8 @@ end --- Orbit at a specified position at a specified alititude with a specified speed. -- @param #AIRBOSS self --- @param Wrapper.Group#GROUP group Group -function AIRBOSS:_MarshalPlayer(group, stack) +-- @param Wrapper.Group#GROUP group Group containing the player unit. +function AIRBOSS:_MarshalPlayer(group) -- Flight group name. local groupname=group:GetName() @@ -1000,9 +1074,10 @@ function AIRBOSS:_CollapseMarshalStack() -- TODO: better message. MESSAGE:New(string.format("Marshal, %s, you are cleared for Case I recovery pattern!", flight.groupname), 15):ToAll() + -- Set player step to 0. if flight.ai==false then local playerData=self:_GetPlayerDataGroup(flight.group) - playerData.step=0 + playerData.step=AIRBOSS.PatternStep.UNREGISTERED end -- Time stamp. @@ -1129,58 +1204,76 @@ function AIRBOSS:_CheckPlayerStatus() playerData.step=90 self.groovedebug=false end - elseif playerData.step==1 then + elseif playerData.step==AIRBOSS.PatternStep.HOLDING then - -- Entering the pattern. - self:_Start(playerData) + elseif playerData.step==AIRBOSS.PatternStep.DESCENT4K then + + elseif playerData.step==AIRBOSS.PatternStep.DESCENT2K then + + elseif playerData.step==AIRBOSS.PatternStep.DIRTYUP then + + elseif playerData.step==AIRBOSS.PatternStep.BULLSEYE then + + elseif playerData.step==AIRBOSS.PatternStep.INITIAL then + + -- Player is at the initial position entering the landing pattern. + self:_Initial(playerData) - elseif playerData.step==2 then + elseif playerData.step==AIRBOSS.PatternStep.UPWIND then - -- Upwind leg. + -- Upwind leg aka break entry. self:_Upwind(playerData) - elseif playerData.step==3 then + elseif playerData.step==AIRBOSS.PatternStep.EARLYBREAK then -- Early break. self:_Break(playerData, "early") - elseif playerData.step==4 then + elseif playerData.step==AIRBOSS.PatternStep.LATEBREAK then -- Late break. self:_Break(playerData, "late") - elseif playerData.step==5 then + elseif playerData.step==AIRBOSS.PatternStep.ABEAM then -- Abeam position. self:_Abeam(playerData) - elseif playerData.step==6 then + elseif playerData.step==AIRBOSS.PatternStep.NINETY then -- Check long down wind leg. self:_CheckForLongDownwind(playerData) + -- At the ninety. self:_Ninety(playerData) - elseif playerData.step==7 then + elseif playerData.step==AIRBOSS.PatternStep.WAKE then -- In the wake. self:_Wake(playerData) - elseif playerData.step==90 then + elseif playerData.step==AIRBOSS.PatternStep.FINAL then -- Entering the groove. - self:_Groove(playerData) + self:_Final(playerData) - elseif playerData.step>=91 and playerData.step<=99 then + elseif playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_RB or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then -- In the groove. - self:_CallTheBall(playerData) + self:_Groove(playerData) - elseif playerData.step==999 then + elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then - -- Debriefing. + -- Debriefing in 10 seconds. SCHEDULER:New(nil, self._Debrief, {self, playerData}, 10) - playerData.step=-999 + + -- Undefined status. + playerData.step=AIRBOSS.PatternStep.UNDEFINED end @@ -1295,7 +1388,7 @@ function AIRBOSS:OnEventLand(EventData) playerData.landed=true -- Unkonwn step. - playerData.step=-999 + playerData.step=AIRBOSS.PatternStep.UNDEFINED --TODO: maybe check that we actually landed on the right carrier. @@ -1436,7 +1529,7 @@ end --- Start pattern when player enters the initial zone. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Start(playerData) +function AIRBOSS:_Initial(playerData) -- Check if player is in start zone and about to enter the pattern. if playerData.unit:IsInZone(self.startZone) then @@ -1451,12 +1544,149 @@ function AIRBOSS:_Start(playerData) self:_SendMessageToPlayer(hint, 8, playerData) -- Next step: upwind. - playerData.step=2 + playerData.step=AIRBOSS.PatternStep.UPWIND end -end +end ---- Upwind leg. +--- Descent at 4k. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Descent4k(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Abort condition check. + if self:_CheckAbort(X, Z, self.C3Descent4k) then + self:_AbortPattern(playerData, X, Z, self.C3Descent4k) + return + end + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, self.C3Descent4k) then + + -- Get altitiude. + local altitude=playerData.unit:GetAltitude() + + -- Get altitude. + local hint, debrief=self:_AltitudeCheck(playerData, self.C3Descent4k, altitude) + + -- Message to player + self:_SendMessageToPlayer(hint, 10, playerData) + + -- Debrief. + self:_AddToSummary(playerData, "Descent 4k", debrief) + + -- Next step: Early Break. + playerData.step=AIRBOSS.PatternStep.DESCENT2K + end +end + +--- Descent at 2k. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Descent2k(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Abort condition check. + if self:_CheckAbort(X, Z, self.C3Descent2k) then + self:_AbortPattern(playerData, X, Z, self.C3Descent2k) + return + end + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, self.C3Descent2k) then + + -- Get altitiude. + local altitude=playerData.unit:GetAltitude() + + -- Get altitude. + local hint, debrief=self:_AltitudeCheck(playerData, self.C3Descent2k, altitude) + + -- Message to player + self:_SendMessageToPlayer(hint, 10, playerData) + + -- Debrief. + self:_AddToSummary(playerData, "Descent 2k", debrief) + + -- Next step: Early Break. + playerData.step=AIRBOSS.PatternStep.DIRTYUP + end +end + +--- Dirty up. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_DirtyUp(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Abort condition check. + if self:_CheckAbort(X, Z, self.C3DirtyUp) then + self:_AbortPattern(playerData, X, Z, self.C3DirtyUp) + return + end + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, self.C3DirtyUp) then + + -- Get altitiude. + local altitude=playerData.unit:GetAltitude() + + -- Get altitude. + local hint, debrief=self:_AltitudeCheck(playerData, self.C3DirtyUp, altitude) + + -- Message to player + self:_SendMessageToPlayer(hint, 10, playerData) + + -- Debrief. + self:_AddToSummary(playerData, "Dirty Up", debrief) + + -- Next step: Early Break. + playerData.step=AIRBOSS.PatternStep.BULLSEYE + end +end + +--- Bulls eye. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_BullsEye(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Abort condition check. + if self:_CheckAbort(X, Z, self.C3DirtyUp) then + self:_AbortPattern(playerData, X, Z, self.C3BullsEye) + return + end + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, self.C3BullsEye) then + + -- Get altitiude. + local altitude=playerData.unit:GetAltitude() + + -- Get altitude. + local hint, debrief=self:_AltitudeCheck(playerData, self.C3BullsEye, altitude) + + -- Message to player + self:_SendMessageToPlayer(hint, 10, playerData) + + -- Debrief. + self:_AddToSummary(playerData, "Bulls Eye", debrief) + + -- Next step: Early Break. + playerData.step=AIRBOSS.PatternStep.FINAL + end +end + + +--- Upwind leg or break entry. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Upwind(playerData) @@ -1485,8 +1715,8 @@ function AIRBOSS:_Upwind(playerData) -- Debrief. self:_AddToSummary(playerData, "Entering the Break", debrief) - -- Next step. - playerData.step=3 + -- Next step: Early Break. + playerData.step=AIRBOSS.PatternStep.EARLYBREAK end end @@ -1531,11 +1761,11 @@ function AIRBOSS:_Break(playerData, part) self:_AddToSummary(playerData, "Late Break", debrief) end - -- Next step: late break or abeam. + -- Next step: Late Break or Abeam. if part=="early" then - playerData.step = 4 + playerData.step=AIRBOSS.PatternStep.LATEBREAK else - playerData.step = 5 + playerData.step=AIRBOSS.PatternStep.ABEAM end end end @@ -1574,12 +1804,12 @@ function AIRBOSS:_CheckForLongDownwind(playerData) playerData.lig=true -- Next step: Debriefing. - playerData.step=999 + playerData.step=AIRBOSS.PatternStep.DEBRIEF end end ---- Abeam. +--- Abeam position. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Abeam(playerData) @@ -1620,11 +1850,11 @@ function AIRBOSS:_Abeam(playerData) self:_AddToSummary(playerData, "Abeam Position", debrief) -- Next step: ninety. - playerData.step=6 + playerData.step=AIRBOSS.PatternStep.NINETY end end ---- Ninety. +--- At the Ninety. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Ninety(playerData) @@ -1665,7 +1895,7 @@ function AIRBOSS:_Ninety(playerData) self:_AddToSummary(playerData, "At the 90", debrief) -- Next step: wake. - playerData.step=7 + playerData.step=AIRBOSS.PatternStep.WAKE elseif relheading>90 and self:_CheckLimits(X, Z, self.Wake) then -- Message to player. @@ -1673,7 +1903,7 @@ function AIRBOSS:_Ninety(playerData) end end ---- Wake. +--- At the Wake. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Wake(playerData) @@ -1710,15 +1940,15 @@ function AIRBOSS:_Wake(playerData) -- Add to debrief. self:_AddToSummary(playerData, "At the Wake", debrief) - -- Next step: Groove. - playerData.step=90 + -- Next step: Final. + playerData.step=AIRBOSS.PatternStep.FINAL end end ---- Entering the Groove. +--- Turn to final. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Groove(playerData) +function AIRBOSS:_Final(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi = self:_GetDistances(playerData.unit) @@ -1729,8 +1959,8 @@ function AIRBOSS:_Groove(playerData) return end - local relhead=self:_GetRelativeHeading(playerData.unit)+self.rwyangle - local lineup=self:_Lineup(playerData)-self.rwyangle + local relhead=self:_GetRelativeHeading(playerData.unit)+self.carrierparam.rwyangle + local lineup=self:_Lineup(playerData)-self.carrierparam.rwyangle local roll=playerData.unit:GetRoll() env.info(string.format("FF relhead=%d lineup=%d roll=%d", relhead, lineup, roll)) @@ -1763,23 +1993,23 @@ function AIRBOSS:_Groove(playerData) groovedata.Alt=alt groovedata.AoA=aoa groovedata.GSE=self:_Glideslope(playerData)-3.5 - groovedata.LUE=self:_Lineup(playerData)-self.rwyangle + groovedata.LUE=self:_Lineup(playerData)-self.carrierparam.rwyangle groovedata.Roll=roll -- Groove playerData.groove.X0=groovedata -- Next step: X start & call the ball. - playerData.step=91 + playerData.step=AIRBOSS.PatternStep.GROOVE_XX end end ---- Call the ball, i.e. 3/4 NM distance between aircraft and carrier. +--- In the groove. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_CallTheBall(playerData) +function AIRBOSS:_Groove(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi = self:_GetDistances(playerData.unit) @@ -1798,7 +2028,7 @@ function AIRBOSS:_CallTheBall(playerData) -- Lineup with runway centerline. local lineup=self:_Lineup(playerData) - local lineupError=lineup-self.rwyangle + local lineupError=lineup-self.carrierparam.rwyangle -- Glide slope. local glideslope=self:_Glideslope(playerData) @@ -1808,7 +2038,7 @@ function AIRBOSS:_CallTheBall(playerData) local AoA=playerData.unit:GetAoA() -- Ranges in the groove. - local RXX=UTILS.NMToMeters(0.750)+math.abs(self.sterndist) -- Start of groove. 0.75 = 1389 m + local RXX=UTILS.NMToMeters(0.750)+math.abs(self.carrierparam.sterndist) -- Start of groove. 0.75 = 1389 m local RRB=UTILS.NMToMeters(0.500)+math.abs(self.sterndist) -- Roger Ball! call. 0.5 = 926 m local RIM=UTILS.NMToMeters(0.375)+math.abs(self.sterndist) -- In the Middle 0.75/2. 0.375 = 695 m local RIC=UTILS.NMToMeters(0.100)+math.abs(self.sterndist) -- In Close. 0.1 = 185 m @@ -1823,7 +2053,7 @@ function AIRBOSS:_CallTheBall(playerData) groovedata.LUE=lineupError groovedata.Roll=playerData.unit:GetRoll() - if rho<=RXX and playerData.step==91 then + if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX then -- LSO "Call the ball" call. self:_SendMessageToPlayer("Call the ball.", 8, playerData) @@ -1834,9 +2064,9 @@ function AIRBOSS:_CallTheBall(playerData) playerData.groove.XX=groovedata -- Next step: roger ball. - playerData.step=92 + playerData.step=AIRBOSS.PatternStep.GROOVE_RB - elseif rho<=RRB and playerData.step==92 then + elseif rho<=RRB and playerData.step==AIRBOSS.PatternStep.GROOVE_RB then -- Pilot: "Roger ball" call. self:_SendMessageToPlayer(AIRBOSS.LSOcall.ROGERBALLT, 8, playerData) @@ -1847,9 +2077,9 @@ function AIRBOSS:_CallTheBall(playerData) playerData.groove.RB=groovedata -- Next step: in the middle. - playerData.step=93 + playerData.step=AIRBOSS.PatternStep.GROOVE_IM - elseif rho<=RIM and playerData.step==93 then + elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then -- Debug. self:_SendMessageToPlayer("IM", 8, playerData) @@ -1859,9 +2089,9 @@ function AIRBOSS:_CallTheBall(playerData) playerData.groove.IM=groovedata -- Next step: in close. - playerData.step=94 + playerData.step=AIRBOSS.PatternStep.GROOVE_IC - elseif rho<=RIC and playerData.step==94 then + elseif rho<=RIC and playerData.step==AIRBOSS.PatternStep.GROOVE_IC then -- Check if player was already waved off. if playerData.waveoff==false then @@ -1890,12 +2120,12 @@ function AIRBOSS:_CallTheBall(playerData) return else -- Next step: AR at the ramp. - playerData.step=95 + playerData.step=AIRBOSS.PatternStep.GROOVE_AR end end - elseif rho<=RAR and playerData.step==95 then + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AR then -- Debug. self:_SendMessageToPlayer("AR", 8, playerData) @@ -1905,7 +2135,7 @@ function AIRBOSS:_CallTheBall(playerData) playerData.groove.AR=groovedata -- Next step: in the wires. - playerData.step=96 + playerData.step=AIRBOSS.PatternStep.GROOVE_IW end -- Time since last LSO call. @@ -1916,7 +2146,7 @@ function AIRBOSS:_CallTheBall(playerData) if rho>=RAR and rho=3 then -- LSO call if necessary. - self:_LSOcall(playerData, glideslopeError, lineupError) + self:_LSOadvice(playerData, glideslopeError, lineupError) elseif X>100 then @@ -1935,7 +2165,8 @@ function AIRBOSS:_CallTheBall(playerData) self:_AddToSummary(playerData, "Wave Off", "You were waved off.") -- Next step: debrief. - playerData.step=999 + playerData.step=AIRBOSS.PatternStep.DEBRIEF + end end end @@ -2050,30 +2281,43 @@ function AIRBOSS:_Trapped(playerData, pos) playerData.step=999 end ---- Entering the Groove. +--- LSO advice call. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -- @param #number glideslopeError Error in degrees. -- @param #number lineupError Error in degrees. -function AIRBOSS:_LSOcall(playerData, glideslopeError, lineupError) +function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- Player group. local player=playerData.unit:GetGroup() + -- List of calls + local calls={} + + local delay=0 + -- Glideslope high/low calls. local text="" if glideslopeError>1 then - text="You're high!" - AIRBOSS.LSOcall.HIGHL:ToGroup(player) + --text="You're high!" + --AIRBOSS.LSOcall.HIGHL:ToGroup(player) + self:RadioTransmission(self.LSOradio, self.radiocall.HIGH, true, delay) + delay=delay+1.5 elseif glideslopeError>0.5 then - text="You're a little high." - AIRBOSS.LSOcall.HIGHS:ToGroup(player) + --text="You're a little high." + --AIRBOSS.LSOcall.HIGHS:ToGroup(player) + self:RadioTransmission(self.LSOradio, self.radiocall.HIGH, false, delay) + delay=delay+1.5 elseif glideslopeError<-1.0 then - text="Power!" - AIRBOSS.LSOcall.POWERL:ToGroup(player) + --text="Power!" + --AIRBOSS.LSOcall.POWERL:ToGroup(player) + self:RadioTransmission(self.LSOradio, self.radiocall.POWER, true, delay) + delay=delay+1.5 elseif glideslopeError<-0.5 then - text="You're a little low." - AIRBOSS.LSOcall.POWERS:ToGroup(player) + --text="You're a little low." + --AIRBOSS.LSOcall.POWERS:ToGroup(player) + self:RadioTransmission(self.LSOradio, self.radiocall.POWER, false, delay) + delay=delay+1.5 else text="Good altitude." end @@ -2081,25 +2325,27 @@ function AIRBOSS:_LSOcall(playerData, glideslopeError, lineupError) text=text..string.format(" Glideslope Error = %.2f°", glideslopeError) text=text.."\n" - local delay=0 - if math.abs(glideslopeError)>0.5 then - --text=text.."\n" - delay=1.5 - end - -- Lineup left/right calls. if lineupError<-3 then - text=text.."Come left!" - AIRBOSS.LSOcall.COMELEFTL:ToGroup(player, delay) + --text=text.."Come left!" + --AIRBOSS.LSOcall.COMELEFTL:ToGroup(player, delay) + self:RadioTransmission(self.LSOradio, self.radiocall.COMELEFT, true, delay) + delay=delay+1.5 elseif lineupError<-1 then - text=text.."Come left." - AIRBOSS.LSOcall.COMELEFTS:ToGroup(player, delay) + --text=text.."Come left." + --AIRBOSS.LSOcall.COMELEFTS:ToGroup(player, delay) + self:RadioTransmission(self.LSOradio, self.radiocall.COMELEFT, false, delay) + delay=delay+1.5 elseif lineupError>3 then - text=text.."Right for lineup!" - AIRBOSS.LSOcall.RIGHTFORLINEUPL:ToGroup(player, delay) + --text=text.."Right for lineup!" + --AIRBOSS.LSOcall.RIGHTFORLINEUPL:ToGroup(player, delay) + self:RadioTransmission(self.LSOradio, self.radiocall.RIGHTFORLINEUP, true, delay) + delay=delay+1.5 elseif lineupError>1 then - text=text.."Right for lineup." - AIRBOSS.LSOcall.RIGHTFORLINEUPS:ToGroup(player, delay) + --text=text.."Right for lineup." + --AIRBOSS.LSOcall.RIGHTFORLINEUPS:ToGroup(player, delay) + self:RadioTransmission(self.LSOradio, self.radiocall.RIGHTFORLINEUP, false, delay) + delay=delay+1.5 else text=text.."Good lineup." end @@ -2110,15 +2356,23 @@ function AIRBOSS:_LSOcall(playerData, glideslopeError, lineupError) local aoa=playerData.unit:GetAoA() if aoa>=9.3 then - text=text.."Your're slow!" + --text=text.."Your're slow!" + self:RadioTransmission(self.LSOradio, self.radiocall.SLOW, true, delay) + delay=delay+1.5 elseif aoa>=8.8 and aoa<9.3 then - text=text.."Your're a little slow." + self:RadioTransmission(self.LSOradio, self.radiocall.SLOW, false, delay) + delay=delay+1.5 + --text=text.."Your're a little slow." elseif aoa>=7.4 and aoa<8.8 then text=text.."You're on speed." elseif aoa>=6.9 and aoa<7.4 then - text=text.."You're a little fast." + --text=text.."You're a little fast." + self:RadioTransmission(self.LSOradio, self.radiocall.FAST, false, delay) + delay=delay+1.5 elseif aoa>=0 and aoa<6.9 then text=text.."You're fast!" + self:RadioTransmission(self.LSOradio, self.radiocall.FALSE, true, delay) + delay=delay+1.5 else text=text.."Unknown AoA state." end @@ -2127,11 +2381,39 @@ function AIRBOSS:_LSOcall(playerData, glideslopeError, lineupError) -- LSO Message to player. self:_SendMessageToPlayer(text, 5, playerData, false) - + -- Set last time. playerData.Tlso=timer.getTime() end +--- Radio transmission. +-- @param #AIRBOSS self +-- @param Core.Radio#RADIO radio sending transmission. +-- @param #AIRBOSS.RadioSound call Radio sound files and subtitles. +-- @param #boolean loud If true, play loud sound file version. +-- @param #number delay Delay in seconds, before the message is broadcasted. +function AIRBOSS:RadioTransmission(radio, call, loud, delay) + + if delay==nil or delay and delay==0 then + + local filename=call.soundfilenormal + if loud then + filename=call.soundfileloud + end + + -- New transmission. + radio:NewUnitTransmission(filename, call.subtitle, call.subtitleduration, radio.Frequency, radio.Modulation, false) + + -- Broadcast message. + radio:Broadcast() + + else + + -- Scheduled transmission. + SCHEDULER:New(nil, self.RadioCall, {self, radio, call, loud}, delay) + end +end + --- Get glide slope of aircraft. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -2314,51 +2596,6 @@ function AIRBOSS:_GetRelativeHeading(unit) return math.deg(relHead) end - ---- Get name of the current pattern step. --- @param #AIRBOSS self --- @param #number step Step --- @return #string Name of the step -function AIRBOSS:_StepName(step) - - local name="unknown" - if step==0 then - name="Unregistered" - elseif step==1 then - name="Pattern Entry" - elseif step==2 then - name="Break Entry" - elseif step==3 then - name="Early break" - elseif step==4 then - name="Late break" - elseif step==5 then - name="Abeam position" - elseif step==6 then - name="Ninety" - elseif step==7 then - name="Wake" - elseif step==8 then - name="unkown" - elseif step==90 then - name="Entering the Groove" - elseif step==91 then - name="Groove: X At the Start" - elseif step==92 then - name="Groove: Roger Ball" - elseif step==93 then - name="Groove: IM In the Middle" - elseif step==94 then - name="Groove: IC In Close" - elseif step==95 then - name="Groove: AR: At the Ramp" - elseif step==96 then - name="Groove: IW: In the Wires" - end - - return name -end - --- Calculate distances between carrier and player unit. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Player unit @@ -2484,7 +2721,7 @@ function AIRBOSS:_AbortPattern(playerData, X, Z, posData) --MESSAGE:New(text, 60):ToAllIf(self.Debug) -- Add to debrief. - self:_AddToSummary(playerData, string.format("%s", self:_StepName(playerData.step)), string.format("Pattern wave off: %s", toofartext)) + self:_AddToSummary(playerData, string.format("%s", playerData.step), string.format("Pattern wave off: %s", toofartext)) -- Pattern wave off! playerData.patternwo=true @@ -2532,7 +2769,7 @@ function AIRBOSS:_DetailedPlayerStatus(playerData) text=text..string.format("Lineup Error = %.1f°\n", lineup) text=text..string.format("Glideslope Error = %.1f°\n", glideslope) end - text=text..string.format("Current step: %s\n", self:_StepName(playerData.step)) + text=text..string.format("Current step: %s\n", playerData.step) --text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) --text=text..string.format("rho=%.1f m phi=%.1f degrees\n", rho,phi) @@ -2545,13 +2782,13 @@ end function AIRBOSS:_InitStennis() -- Carrier Parameters. - self.rwyangle = -9 - self.sterndist =-150 - self.deckheight = 22 - self.wire1 =-100 - self.wire2 = -90 - self.wire3 = -80 - self.wire4 = -70 + self.carrierparam.rwyangle = -9 + self.carrierparam.sterndist =-150 + self.carrierparam.deckheight = 22 + self.carrierparam.wire1 =-100 + self.carrierparam.wire2 = -90 + self.carrierparam.wire3 = -80 + self.carrierparam.wire4 = -70 --[[ q0=self.carrier:GetCoordinate():SetAltitude(25) From 34d7b18c260a2412da9a1dc24a597abb3935512f Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 14 Nov 2018 23:23:46 +0100 Subject: [PATCH 042/147] AIRBOSS v0.2.6 --- .../Moose/Functional/CarrierTrainer.lua | 336 +++++++++++------- 1 file changed, 208 insertions(+), 128 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 3f6975264..cdd8ba769 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -14,8 +14,9 @@ -- * Automatic TACAN and ICLS channel setting. -- * Different radio channels for LSO and airboss calls. -- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels, pilot grades). +-- * Multiple carriers supported. -- --- Please not that his class is work in progress and in an **alpha** stage. +-- **PLEASE NOTE** 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. -- @@ -44,7 +45,7 @@ -- @field Core.Radio#RADIO Carrierradio Radio for carrier calls. -- @field #AIRBOSS.RadioCalls radiocall LSO and Airboss call sound files and texts. -- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. --- @field Core.Zone#ZONE_UNIT carrierZone Large zone around the carrier to welcome players. +-- @field Core.Zone#ZONE_UNIT carrierZone Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT registerZone Zone behind the carrier to register for a new approach. -- @field Core.Zone#ZONE_UNIT zoneHolding Zone where aircraft are holding before entering the landing pattern. -- @field #table players Table of players. @@ -60,10 +61,7 @@ -- @field #AIRBOSS.Checkpoint C3Descent4k Case III descent at 4000 ft/min right after leaving holding pattern. -- @field #AIRBOSS.Checkpoint C3Descent2k Case III descent at 2000 ft/min at 5000 ft plattform. -- @field #AIRBOSS.Checkpoint C3DirtyUp Case III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. --- @field #AIRBOSS.Checkpoint C3BullsEye Case III intercept glideslope and follow ICLS "bullseye". --- @field #number rwyangle Angle of the runway wrt to carrier "nose". For the Stennis ~ -10 degrees. --- @field #number sterndist Distance in meters from carrier coordinate to the end of the deck. --- @field #number deckheight Height of the deck in meters. +-- @field #AIRBOSS.Checkpoint C3BullsEye Case III intercept glideslope and follow ICLS "bullseye". -- @field #number case Recovery case I, II or III. -- @field #table Qmarshal Queue of marshalling aircraft groups. -- @field #table Qpattern Queue of aircraft groups in the landing pattern. @@ -89,7 +87,7 @@ AIRBOSS = { Debug = true, carrier = nil, carriertype = nil, - carrierparam = nil, + carrierparam = {}, alias = nil, airbase = nil, beacon = nil, @@ -119,7 +117,6 @@ AIRBOSS = { C3Descent2k = {}, C3DirtyUp = {}, C3BullsEye = {}, - radiocall = nil, case = 1, Qpattern = {}, Qmarshal = {}, @@ -164,7 +161,7 @@ AIRBOSS.CarrierType={ -- @type AIRBOSS.PatternStep AIRBOSS.PatternStep={ UNDEFINED="Undefined", - UNREGISTERED="Unregistered", + COMMENCING="Commencing", HOLDING="Holding", DESCENT4K="Descent 4000 ft/min", DESCENT2K="Descent 2000 ft/min", @@ -212,11 +209,13 @@ AIRBOSS.PatternStep={ -- @type AIRBOSS.Soundfile -- @field #AIRBOSS.RadioSound RIGHTFORLINEUP -- @field #AIRBOSS.RadioSound COMELEFT --- @field #AIRBOSS.RadioSound --- @field #AIRBOSS.RadioSound --- @field #AIRBOSS.RadioSound --- @field #AIRBOSS.RadioSound --- @field #AIRBOSS.RadioSound +-- @field #AIRBOSS.RadioSound HIGH +-- @field #AIRBOSS.RadioSound POWER +-- @field #AIRBOSS.RadioSound CALLTHEBALL +-- @field #AIRBOSS.RadioSound ROGERBALL +-- @field #AIRBOSS.RadioSound WAVEOFF +-- @field #AIRBOSS.RadioSound BOLTER +-- @field #AIRBOSS.RadioSound LONGINGROOVE AIRBOSS.Soundfile={ RIGHTFORLINEUP={ normal="LSO - RightLineUp(L).ogg", @@ -328,6 +327,7 @@ AIRBOSS.GroovePos={ -- @field Wrapper.Group#GROUP group Aircraft group the player is in. -- @field #string callsign Callsign of player. -- @field #string difficulty Difficulty level. +-- @field #string step Coming pattern step. -- @field #number passes Number of passes. -- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. -- @field #table debrief Debrief analysis of the current step of this pass. @@ -380,13 +380,14 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.2.5w" +AIRBOSS.version="0.2.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Handle crash event. Delete ac from queue, send rescue helo, stop carrier? +-- TODO: Handle crash event. Delete A/C from queue, send rescue helo, stop carrier? +-- TODO: Add aircraft numbers in queue to carrier info F10 radio output. -- TODO: Transmission via radio. -- TODO: Get board numbers. -- TODO: Get fuel state in pounds. @@ -444,9 +445,11 @@ function AIRBOSS:New(carriername, alias) -- Set up Airboss radio. self.Carrierradio=RADIO:New(self.carrier) + self:SetCarrierradio() -- Set up LSO radio. self.LSOradio=RADIO:New(self.carrier) + self:SetLSOradio() -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then @@ -465,11 +468,11 @@ function AIRBOSS:New(carriername, alias) return nil end - -- Zone 5 km astern and 100 m starboard of the carrier with radius of 2.5 km. - self.registerZone = ZONE_UNIT:New("registerZone", self.carrier, 2.5*1000, {dx = -5000, dy = 100, relative_to_unit=true}) + -- Zone 3 NM astern and 100 m starboard of the carrier with radius of 2.0 km. + self.zoneInitial=ZONE_UNIT:New("registerZone", self.carrier, 2.0*1000, {dx=-UTILS.NMToMeters(3), dy=100, relative_to_unit=true}) -- Zone 2 km astern and 100 m starboard of the carrier with a radius of 1 km. - self.startZone = ZONE_UNIT:New("startZone", self.carrier, 1.0*1000, {dx = -2000, dy = 100, relative_to_unit=true}) + self.startZone = ZONE_UNIT:New("startZone", self.carrier, 1.0*1000, {dx=-2000, dy=100, relative_to_unit=true}) -- Zone around the carrier with a radius of 30 km. self:SetCarrierControlledZone() @@ -477,6 +480,13 @@ function AIRBOSS:New(carriername, alias) -- Default recovery case. self:SetRecoveryCase(1) + env.info("FF sound files:") + for _name,_sound in pairs(AIRBOSS.Soundfile) do + local sound=_sound --#AIRBOSS.RadioSound + self:I{name=_name,sound=_sound} + self.radiocall[_name]=sound + end + ----------------------- --- FSM Transitions --- ----------------------- @@ -601,22 +611,26 @@ end --- Set LSO radio frequency. -- @param #AIRBOSS self --- @param #number freq Frequency in MHz. Default 264 MHz. +-- @param #number frequency Frequency in MHz. Default 264 MHz. +-- @param #string modulation Modulation, i.e. "AM" (default) or "FM". -- @return #AIRBOSS self -function AIRBOSS:SetLSOradio(freq) +function AIRBOSS:SetLSOradio(frequency, modulation) - self.LSOfreq=freq or 264 + self.LSOfreq=frequency or 264 + self.LSOmodulation=modulation or "AM" return self end --- Set carrier radio frequency. -- @param #AIRBOSS self --- @param #number freq Frequency in MHz. Default 305. +-- @param #number frequency Frequency in MHz. Default 305 MHz. +-- @param #string modulation Modulation, i.e. "AM" (default) or "FM". -- @return #AIRBOSS self -function AIRBOSS:SetCarrierradio(freq) +function AIRBOSS:SetCarrierradio(frequency, modulation) - self.Carrierfreq=freq or 305 + self.Carrierfreq=frequency or 305 + self.Carrriermodulation=modulation or "AM" return self end @@ -687,12 +701,12 @@ function AIRBOSS:onafterStatus(From, Event, To) if startrecovery==true then self:Recover() end - - local text=string.format("AIRBOSS %s: Status %s.", self.alias, self:GetState()) - self:I(text) -- Update marshal and pattern queue every 30 seconds. if time-self.Tqueue>30 then + + local text=string.format("AIRBOSS %s: Status %s.", self.alias, self:GetState()) + self:I(text) -- Scan carrier zone for new aircraft. self:_ScanCarrierZone() @@ -708,7 +722,7 @@ function AIRBOSS:onafterStatus(From, Event, To) self:_CheckPlayerStatus() -- Call status again in one second. - self:__Status(-1) + self:__Status(-0.5) end --- Check if recovery times. @@ -850,7 +864,7 @@ function AIRBOSS:_PrintQueue(queue, name) end ---- Check if new aircraft arrived +--- Scan carrier zone for (new) units. -- @param #AIRBOSS self function AIRBOSS:_ScanCarrierZone() --env.info("FF Scanning Carrier Zone") @@ -874,6 +888,7 @@ function AIRBOSS:_ScanCarrierZone() -- Check if this an aircraft and that it is airborn and closing in. if unit:IsAir() and unit:InAir() and unit:IsInZone(zsma)then -- TODO: check for correct aircraft types and also helos! + -- TODO: check for right coalition. local group=unit:GetGroup() local unitname=unit:GetName() @@ -1011,7 +1026,7 @@ function AIRBOSS:_MarshalAI(group) end -- Pattern altitude. - local Altitude=UTILS.FeetToMeters((nstacks+angels0)*1000) + local Altitude=UTILS.FeetToMeters((nstacks+angels0)*1000) -- Add group to marshal stack. self:_AddMarshallGroup(group, nstacks+1, Altitude) @@ -1077,7 +1092,7 @@ function AIRBOSS:_CollapseMarshalStack() -- Set player step to 0. if flight.ai==false then local playerData=self:_GetPlayerDataGroup(flight.group) - playerData.step=AIRBOSS.PatternStep.UNREGISTERED + playerData.step=AIRBOSS.PatternStep.COMMENCING end -- Time stamp. @@ -1168,6 +1183,7 @@ function AIRBOSS:_CheckPlayerStatus() -- Player unit. local unit=playerData.unit + -- Check if unit is alive and in air. if unit:IsAlive() then -- Display aircraft attitude and other parameters as message text. @@ -1175,7 +1191,7 @@ function AIRBOSS:_CheckPlayerStatus() self:_DetailedPlayerStatus(playerData) end - -- Check if player is in carrier controlled zone. + -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). if unit:IsInZone(self.carrierZone) then -- Check if player was previously not inside the zone. @@ -1194,26 +1210,48 @@ function AIRBOSS:_CheckPlayerStatus() end - if playerData.step==0 and unit:InAir() then + if playerData.step==AIRBOSS.PatternStep.UNDEFINED then - -- New approach. - self:_NewRound(playerData) + self:I("Player status undefined. Waiting for next step.") - -- Jump to Groove for testing. + -- Jump directly to CASE I straight in approach. + playerData.step=AIRBOSS.PatternStep.COMMENCING + + -- Jump to final/groove for testing. if self.groovedebug then - playerData.step=90 + playerData.step=AIRBOSS.PatternStep.FINAL self.groovedebug=false end + + elseif playerData.step==AIRBOSS.PatternStep.COMMENCING and unit:InAir() then + + -- New approach. + self:_Commencing(playerData) + elseif playerData.step==AIRBOSS.PatternStep.HOLDING then + -- TODO: holding check. + elseif playerData.step==AIRBOSS.PatternStep.DESCENT4K then + -- CASE III: Initial descent with 4000 ft/min. + self:_Descent4k(playerData) + elseif playerData.step==AIRBOSS.PatternStep.DESCENT2K then + -- CASE III: Player has reached 5k "Platform". + self:_Descent2k(playerData) + elseif playerData.step==AIRBOSS.PatternStep.DIRTYUP then + -- CASE III: Player has descended to 1200 ft and is going level from now on. + self:_DirtyUp(playerData) + elseif playerData.step==AIRBOSS.PatternStep.BULLSEYE then + -- CASE III: Player has intercepted the glide slope and should follow "Bullseye" (ICLS). + self:_BullsEye(playerData) + elseif playerData.step==AIRBOSS.PatternStep.INITIAL then -- Player is at the initial position entering the landing pattern. @@ -1254,7 +1292,7 @@ function AIRBOSS:_CheckPlayerStatus() elseif playerData.step==AIRBOSS.PatternStep.FINAL then - -- Entering the groove. + -- Turn to final and enter the groove. self:_Final(playerData) elseif playerData.step==AIRBOSS.PatternStep.GROOVE_XX or @@ -1338,7 +1376,7 @@ function AIRBOSS:OnEventBirth(EventData) self.players[_playername]=self:_InitPlayer(_unitName) -- Start in the groove for debugging. - self.groovedebug=false + self.groovedebug=true end end @@ -1362,43 +1400,50 @@ function AIRBOSS:OnEventLand(EventData) 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) + -- This would be the closest airbase. + local airbase=EventData.Place + local airbasename=tostring(airbase:GetName()) - -- Player data. - local playerData=self.players[_playername] --#AIRBOSS.PlayerData + -- Check if player landed on the right airbase. + if airbasename==self.airbase:GetName() then - -- Coordinate at landing event - local coord=playerData.unit:GetCoordinate() - - -- Debug mark of player landing coord. - local lp=coord:MarkToAll("Landing coord.") - coord:SmokeGreen() - - -- Debug marks of wires. - local w1=self.carrier:GetCoordinate():Translate(-104, 0):MarkToAll("Wire 1") - local w2=self.carrier:GetCoordinate():Translate( -92, 0):MarkToAll("Wire 2") - local w3=self.carrier:GetCoordinate():Translate( -80, 0):MarkToAll("Wire 3") - local w4=self.carrier:GetCoordinate():Translate( -68, 0):MarkToAll("Wire 4") - - -- We did land. - env.info("FF landed") - playerData.landed=true - - -- Unkonwn step. - playerData.step=AIRBOSS.PatternStep.UNDEFINED - - --TODO: maybe check that we actually landed on the right carrier. - - -- Call trapped function in 3 seconds to make sure we did not bolter. - SCHEDULER:New(nil, self._Trapped,{self, playerData, coord}, 3) + -- Debug output. + local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) - end - - if self:_InQueue(self.Qpattern, EventData.IniGroup) then - self:_RemoveQueue(self.Qpattern, EventData.IniGroup) + -- Player data. + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + -- Coordinate at landing event + local coord=playerData.unit:GetCoordinate() + + -- Debug mark of player landing coord. + local lp=coord:MarkToAll("Landing coord.") + coord:SmokeGreen() + + -- Debug marks of wires. + local w1=self.carrier:GetCoordinate():Translate(self.carrierparam.wire1, 0):MarkToAll("Wire 1") + local w2=self.carrier:GetCoordinate():Translate(self.carrierparam.wire2, 0):MarkToAll("Wire 2") + local w3=self.carrier:GetCoordinate():Translate(self.carrierparam.wire3, 0):MarkToAll("Wire 3") + local w4=self.carrier:GetCoordinate():Translate(self.carrierparam.wire4, 0):MarkToAll("Wire 4") + + -- We did land. + env.info("FF landed") + playerData.landed=true + + -- Unkonwn step. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + -- Call trapped function in 3 seconds to make sure we did not bolter. + SCHEDULER:New(nil, self._Trapped,{self, playerData, coord}, 3) + + end + + if self:_InQueue(self.Qpattern, EventData.IniGroup) then + self:_RemoveQueue(self.Qpattern, EventData.IniGroup) + end + end end @@ -1482,7 +1527,7 @@ function AIRBOSS:_InitPlayer(unitname) playerData.inbigzone=playerData.unit:IsInZone(self.carrierZone) -- Init stuff for this round. - playerData=self:_InitNewRound(playerData) + playerData=self:_InitNewApproach(playerData) -- Return player data table. return playerData @@ -1495,9 +1540,11 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -- @return #AIRBOSS.PlayerData Initialized player data. -function AIRBOSS:_InitNewRound(playerData) - self:I(self.lid..string.format("New round for player %s.", playerData.callsign)) - playerData.step=0 +function AIRBOSS:_InitNewApproach(playerData) + self:I(self.lid..string.format("New approach of player %s.", playerData.callsign)) + + playerData.step=AIRBOSS.PatternStep.UNDEFINED + playerData.groove={} playerData.debrief={} playerData.patternwo=false @@ -1507,23 +1554,30 @@ function AIRBOSS:_InitNewRound(playerData) playerData.boltered=false playerData.landed=false playerData.Tlso=timer.getTime() + return playerData end ---- Initialize player data. +--- Commence approach. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_NewRound(playerData) - - if playerData.unit:IsInZone(self.registerZone) then - local text="Cleared for approach." - self:_SendMessageToPlayer(text, 10, playerData) +function AIRBOSS:_Commencing(playerData) + + local text="Commencing." + self:_SendMessageToPlayer(text, 10, playerData) - self:_InitNewRound(playerData) - - -- Next step: start of pattern. - playerData.step=1 + -- Initialize player data for new approach. + self:_InitNewApproach(playerData) + + -- Next step: depends on case recovery. + if self.case==1 then + -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. + playerData.step=AIRBOSS.PatternStep.INITIAL + else + -- CASE III: Player has to start the descent at 4000 ft/min. + playerData.step=AIRBOSS.PatternStep.DESCENT4K end + end --- Start pattern when player enters the initial zone. @@ -1531,8 +1585,8 @@ end -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Initial(playerData) - -- Check if player is in start zone and about to enter the pattern. - if playerData.unit:IsInZone(self.startZone) then + -- Check if player is in initial zone and entering the CASE I pattern. + if playerData.unit:IsInZone(self.zoneInitial) then -- Inform player. local hint = string.format("Entering the pattern.") @@ -1541,7 +1595,7 @@ function AIRBOSS:_Initial(playerData) end -- Send message. - self:_SendMessageToPlayer(hint, 8, playerData) + self:_SendMessageToPlayer(hint, 10, playerData) -- Next step: upwind. playerData.step=AIRBOSS.PatternStep.UPWIND @@ -1790,12 +1844,9 @@ function AIRBOSS:_CheckForLongDownwind(playerData) -- Check we are not too far out w.r.t back of the boat. if X Date: Thu, 15 Nov 2018 16:11:14 +0100 Subject: [PATCH 043/147] AIRBOSS v0.2.6w --- .../Moose/Functional/CarrierTrainer.lua | 1465 +++++++++-------- 1 file changed, 795 insertions(+), 670 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index cdd8ba769..739bd1039 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -44,10 +44,10 @@ -- @field Core.Radio#RADIO LSOradio Radio for LSO calls. -- @field Core.Radio#RADIO Carrierradio Radio for carrier calls. -- @field #AIRBOSS.RadioCalls radiocall LSO and Airboss call sound files and texts. --- @field Core.Zone#ZONE_UNIT startZone Zone in which the pattern approach starts. --- @field Core.Zone#ZONE_UNIT carrierZone Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. --- @field Core.Zone#ZONE_UNIT registerZone Zone behind the carrier to register for a new approach. +-- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. +-- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneHolding Zone where aircraft are holding before entering the landing pattern. +-- @field Core.Zone#ZONE_UNIT zoneInitial Zone usually 3 NM astern of carrier where pilots start their CASE I pattern. -- @field #table players Table of players. -- @field #table menuadded Table of units where the F10 radio menu was added. -- @field #AIRBOSS.Checkpoint Upwind Upwind checkpoint. @@ -62,7 +62,7 @@ -- @field #AIRBOSS.Checkpoint C3Descent2k Case III descent at 2000 ft/min at 5000 ft plattform. -- @field #AIRBOSS.Checkpoint C3DirtyUp Case III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. -- @field #AIRBOSS.Checkpoint C3BullsEye Case III intercept glideslope and follow ICLS "bullseye". --- @field #number case Recovery case I, II or III. +-- @field #number case Recovery case I or III in progress. -- @field #table Qmarshal Queue of marshalling aircraft groups. -- @field #table Qpattern Queue of aircraft groups in the landing pattern. -- @field #RESCUEHELO rescuehelo Rescue helo flying in close formation with the carrier. @@ -99,10 +99,10 @@ AIRBOSS = { Carrierradio = nil, Carrierfreq = nil, radiocall = {}, - registerZone = nil, - startZone = nil, - carrierZone = nil, - zoneHolding = nil, + zoneCCA = nil, + zoneCCZ = nil, + zoneHolding = nil, + zoneInitial = nil, players = {}, menuadded = {}, Upwind = {}, @@ -373,6 +373,7 @@ AIRBOSS.GroovePos={ -- @field #number time Time the flight was added to the queue. -- @field Core.UserFlag#USERFLAG flag User flag for triggering events for the flight. -- @field #boolean ai If true, flight is AI. If false, flight is a human player. +-- @field #AIRBOSS.PlayerData player Player data for human pilots. --- Main radio menu. -- @field #table MenuF10 @@ -380,12 +381,14 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.2.6" +AIRBOSS.version="0.2.6w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Monitor holding of players/AI in zoneHolding. +-- TODO: Right pattern step after bolter/wo/patternWO? -- TODO: Handle crash event. Delete A/C from queue, send rescue helo, stop carrier? -- TODO: Add aircraft numbers in queue to carrier info F10 radio output. -- TODO: Transmission via radio. @@ -397,6 +400,8 @@ AIRBOSS.version="0.2.6" -- TODO: CASE II. -- TODO: CASE III. -- TODO: Foul deck check. +-- TODO: Persistence of results. +-- TODO: Stike group with helo bringing cargo. -- DONE: Add scoring to radio menu. -- DONE: Optimized debrief. -- DONE: Add automatic grading. @@ -469,17 +474,18 @@ function AIRBOSS:New(carriername, alias) end -- Zone 3 NM astern and 100 m starboard of the carrier with radius of 2.0 km. - self.zoneInitial=ZONE_UNIT:New("registerZone", self.carrier, 2.0*1000, {dx=-UTILS.NMToMeters(3), dy=100, relative_to_unit=true}) + self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, 2.0*1000, {dx=-UTILS.NMToMeters(3), dy=100, relative_to_unit=true}) - -- Zone 2 km astern and 100 m starboard of the carrier with a radius of 1 km. - self.startZone = ZONE_UNIT:New("startZone", self.carrier, 1.0*1000, {dx=-2000, dy=100, relative_to_unit=true}) + -- CCA 50 NM radius zone around the carrier. + self:SetCarrierControlledArea() - -- Zone around the carrier with a radius of 30 km. + -- CCZ 5 NM radius zone around the carrier. self:SetCarrierControlledZone() -- Default recovery case. self:SetRecoveryCase(1) + -- Debug. env.info("FF sound files:") for _name,_sound in pairs(AIRBOSS.Soundfile) do local sound=_sound --#AIRBOSS.RadioSound @@ -538,16 +544,30 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Set carrier controlled zone. --- This is a zone around the carrier which is constantly updated wrt the carrier position. +--- Set carrier controlled area (CCA). +-- This is a large zone around the carrier, which is constantly updated wrt the carrier position. -- @param #AIRBOSS self -- @param #number radius Radius of zone in nautical miles (NM). Default 50 NM. -- @return #AIRBOSS self -function AIRBOSS:SetCarrierControlledZone(radius) +function AIRBOSS:SetCarrierControlledArea(radius) radius=UTILS.NMToMeters(radius or 50) - self.carrierZone=ZONE_UNIT:New("Carrier Controlled Zone", self.carrier, radius) + self.zoneCCA=ZONE_UNIT:New("Carrier Controlled Area", self.carrier, radius) + + return self +end + +--- Set carrier controlled zone (CCZ). +-- This is a small zone (usually 5 NM radius) around the carrier, which is constantly updated wrt the carrier position. +-- @param #AIRBOSS self +-- @param #number radius Radius of zone in nautical miles (NM). Default 5 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCarrierControlledZone(radius) + + radius=UTILS.NMToMeters(radius or 5) + + self.zoneCCZ=ZONE_UNIT:New("Carrier Controlled Zone", self.carrier, radius) return self end @@ -618,6 +638,12 @@ function AIRBOSS:SetLSOradio(frequency, modulation) self.LSOfreq=frequency or 264 self.LSOmodulation=modulation or "AM" + + if modulation=="FM" then + self.LSOmodulation=radio.modulation.FM + else + self.LSOmodulation=radio.modulation.AM + end return self end @@ -631,6 +657,12 @@ function AIRBOSS:SetCarrierradio(frequency, modulation) self.Carrierfreq=frequency or 305 self.Carrriermodulation=modulation or "AM" + + if modulation=="FM" then + self.Carriermodulation=radio.modulation.FM + else + self.Carriermodulation=radio.modulation.AM + end return self end @@ -672,7 +704,7 @@ function AIRBOSS:onafterStart(From, Event, To) -- Activate ICLS. if self.ICLSchannel then self.beacon:ActivateICLS(self.ICLSchannel, "STN") - end + end -- Handle events. self:HandleEvent(EVENTS.Birth) @@ -721,7 +753,7 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Check player status. self:_CheckPlayerStatus() - -- Call status again in one second. + -- Call status every 0.5 seconds. self:__Status(-0.5) end @@ -789,6 +821,205 @@ function AIRBOSS:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Land) end +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Parameter initialization +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Init parameters for USS Stennis carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.rwyangle = -9 + self.carrierparam.sterndist =-150 + self.carrierparam.deckheight = 22 + self.carrierparam.wire1 =-104 + self.carrierparam.wire2 = -92 + self.carrierparam.wire3 = -80 + self.carrierparam.wire4 = -68 + + --[[ + q0=self.carrier:GetCoordinate():SetAltitude(25) + q0:BigSmokeSmall(0.1) + q1=self.carrier:GetCoordinate():Translate(-104,0):SetAltitude(22) --1st wire + q1:BigSmokeSmall(0.1)--:SmokeGreen() + q2=self.carrier:GetCoordinate():Translate(-68,0):SetAltitude(22) --4th wire ==> distance between wires 12 m + q2:BigSmokeSmall(0.1)--:SmokeBlue() + ]] + + -- 4k descent from holding pattern to 5k platform + self.C3Descent4k.name="4k Descent" + self.C3Descent4k.Xmin=-UTILS.NMToMeters(35) + self.C3Descent4k.Xmax=-UTILS.NMToMeters(20) + self.C3Descent4k.Zmin=-UTILS.NMToMeters(30) + self.C3Descent4k.Zmax= UTILS.NMToMeters(30) + self.C3Descent4k.LimitXmin=nil + self.C3Descent4k.LimitXmax=-UTILS.NMToMeters(20) --TODO: better rho dist. decrease descent 20 2000 ft/min at 5000 ft alt and user rad alt. + self.C3Descent4k.LimitZmin=nil + self.C3Descent4k.LimitZmax=nil + self.C3Descent4k.Altitude=nil --UTILS.FeetToMeters(5000) + self.C3Descent4k.AoA=nil + self.C3Descent4k.Distance=nil + + -- 2k descent from 5k platform to 1200 dirty up level flight. + self.C3Descent2k.name="2k Descent" + self.C3Descent2k.Xmin=-UTILS.NMToMeters(21) + self.C3Descent2k.Xmax=nil + self.C3Descent2k.Zmin=-UTILS.NMToMeters(30) + self.C3Descent2k.Zmax= UTILS.NMToMeters(30) + self.C3Descent2k.LimitXmin=nil + self.C3Descent2k.LimitXmax=-UTILS.NMToMeters(12) --TODO: better rho dist! now switch to dirty up level flight 12 NM. + self.C3Descent2k.LimitZmin=nil + self.C3Descent2k.LimitZmax=nil + self.C3Descent2k.Altitude=UTILS.FeetToMeters(5000) + self.C3Descent2k.AoA=nil + self.C3Descent2k.Distance=-UTILS.NMToMeters(20) + + -- Level out at 1200 ft and dirty up. + self.C3DirtyUp.name="Dirty Up" + self.C3DirtyUp.Xmin=-UTILS.NMToMeters(13) + self.C3DirtyUp.Xmax=nil + self.C3DirtyUp.Zmin=-UTILS.NMToMeters(30) + self.C3DirtyUp.Zmax= UTILS.NMToMeters(30) + self.C3DirtyUp.LimitXmin=nil + self.C3DirtyUp.LimitXmax=-UTILS.NMToMeters(3) --TODO: better rho dist! Intercept glideslope and follow bullseye. + self.C3DirtyUp.LimitZmin=nil + self.C3DirtyUp.LimitZmax=nil + self.C3DirtyUp.Altitude=UTILS.FeetToMeters(1200) + self.C3DirtyUp.AoA=nil + self.C3DirtyUp.Distance=-UTILS.NMToMeters(12) + + -- Intercept glide slope and follow bullseye. + self.C3DirtyUp.name="Bullseye" + self.C3DirtyUp.Xmin=-UTILS.NMToMeters(4) + self.C3DirtyUp.Xmax=nil + self.C3DirtyUp.Zmin=-UTILS.NMToMeters(30) + self.C3DirtyUp.Zmax= UTILS.NMToMeters(30) + self.C3DirtyUp.LimitXmin=nil + self.C3DirtyUp.LimitXmax=-UTILS.NMToMeters(1) --TODO: better rho dist! Call the ball. + self.C3DirtyUp.LimitZmin=nil + self.C3DirtyUp.LimitZmax=nil + self.C3DirtyUp.Altitude=UTILS.FeetToMeters(1200) + self.C3DirtyUp.AoA=nil + self.C3DirtyUp.Distance=-UTILS.NMToMeters(3) + + -- Upwind leg + self.Upwind.name="Upwind" + self.Upwind.Xmin=-UTILS.NMToMeters(4) + self.Upwind.Xmax=nil + self.Upwind.Zmin=0 + self.Upwind.Zmax=1000 + 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=UTILS.NMToMeters(5) + 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=UTILS.NMToMeters(5) + 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=-1000 + 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 + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Queues +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Orbit at a specified position at a specified alititude with a specified speed. -- @param #AIRBOSS self @@ -819,7 +1050,7 @@ function AIRBOSS:_CheckQueue() local Tmarshal=timer.getTime()-marshalflight.time env.info(string.format("Marshal time of group %s = %d seconds", marshalflight.groupname, Tmarshal)) - -- Time (last) flight has entered landing pattern. + -- Time (last) flight has entered landing pattern. local Tpattern=999 if npattern>0 then local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.Queueitem @@ -842,7 +1073,7 @@ function AIRBOSS:_CheckQueue() end end ---- Collapse marshal stack. +--- Print holding queue. -- @param #AIRBOSS self -- @param #table queue Queue to print. -- @param #string name Queue name. @@ -872,14 +1103,19 @@ function AIRBOSS:_ScanCarrierZone() -- Carrier position. local coord=self.carrier:GetCoordinate() + local Rout=UTILS.NMToMeters(50) + local Rin=UTILS.NMToMeters(10) + -- Scan units in carrier zone. - local _,_,_,unitscan=coord:ScanObjects(30*1000, true, false, false) + local _,_,_,unitscan=coord:ScanObjects(Rout, true, false, false) -- Inside and outside zones. - local zbig=ZONE_RADIUS:New("Bla1", self.carrier:GetVec2(), 30*1000) - local zsma=ZONE_RADIUS:New("Bla2", self.carrier:GetVec2(), 10*1000) + local zbig=ZONE_RADIUS:New("Bla1", self.carrier:GetVec2(), Rout) + local zsma=ZONE_RADIUS:New("Bla2", self.carrier:GetVec2(), Rin) - -- Check if we scaned already. + local firstscan=self.unitsout==nil + + -- Check if we scanned already. if self.unitsout~=nil then for _,_unit in pairs(self.unitsout) do @@ -920,9 +1156,37 @@ function AIRBOSS:_ScanCarrierZone() env.info(string.format("Possible incoming unit %s", unit:GetName())) table.insert(self.unitsout, unit) end - end + end + end +--- Add a flight group. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @param #number flagvalue Initial user flag value. +-- @param #number alt Altitude in feet. +function AIRBOSS:_AddFlightGroup(group) + + -- Flight group name + local groupname=group:GetName() + + -- Queue table item. + local qitem={} --#AIRBOSS.Queueitem + qitem.group=group + qitem.groupname=group:GetName() + qitem.nunits=#group:GetUnits() + qitem.fuel=group:GetFuelMin() + qitem.time=timer.getTime() + qitem.flag=USERFLAG:New(groupname) + qitem.flag:Set(-100) + qitem.ai=not self:_IsHuman(group) + + if human then + local playerData=self:_GetPlayerDataGroup(group) + qitem.player=playerData + end + +end --- Orbit at a specified position at a specified alititude with a specified speed. -- @param #AIRBOSS self @@ -933,18 +1197,16 @@ function AIRBOSS:_MarshalPlayer(group) local groupname=group:GetName() -- Number of full marshal stacks. - local nstacks=#self.Qmarshal + local nstacks=#self.Qmarshal + + local playerData=self:_GetPlayerDataGroup(group) + if playerData then + --self:_SendMessageToPlayer(message,duration,playerData,clear,sender,delay) + end -- Add group to marshal stack. self:_AddMarshallGroup(group, nstacks+1) - --[[ - local playerData=self:_GetPlayerDataGroup(group) - if playerData then - self:_SendMessageToPlayer(message,duration,playerData,clear,sender,delay) - end - ]] - --TODO: playerData set end @@ -971,7 +1233,7 @@ function AIRBOSS:_MarshalAI(group) DCSTask.id="ControlledTask" DCSTask.params={} DCSTask.params.task=group:TaskOrbit(p1, alt, speed, p2) - DCSTask.params.stopCondition={userFlag=groupname, userFlagValue=stopflag} + DCSTask.params.stopCondition={userFlag=groupname, userFlagValue=stopflag} return DCSTask end @@ -1083,7 +1345,15 @@ function AIRBOSS:_CollapseMarshalStack() flight.flag:Set(flagvalue-1) end + local nmarshal=#self.Qmarshal + + for i=nmarshal,1,-1 do + local flight=self.Qmarshal[i] --#AIRBOSS.Queueitem + --flight. + end + local flight=self.Qmarshal[1] --#AIRBOSS.Queueitem + env.info(string.format("New pattern flight %s.", flight.groupname)) -- TODO: better message. @@ -1105,70 +1375,34 @@ function AIRBOSS:_CollapseMarshalStack() table.remove(self.Qmarshal, 1) end ---- Checks if a group has a human player. +--- Remove a group from a queue. -- @param #AIRBOSS self --- @param Wrapper.Group#GROUP group Aircraft group. --- @return #boolean If true, human player inside group. -function AIRBOSS:_IsHuman(group) +-- @param #table queue The queue from which the group will be removed. +-- @param Wrapper.Group#GROUP group Group that will be removed from queue. +function AIRBOSS:_RemoveQueue(queue, group) - local units=group:GetUnits() - - for _,_unit in pairs(units) do - local playerunit=self:_GetPlayerUnitAndName(_unit:GetName()) - if playerunit then - return true - end - end - - return false -end - ---- Check if a group is in the queue. --- @param #AIRBOSS self --- @param #table queue The queue to check. --- @param Wrapper.Group#GROUP group --- @return #boolean If true, group is in the queue. False otherwise. -function AIRBOSS:_InQueue(queue, group) local name=group:GetName() - for _,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Queueitem - if name==flight.groupname then - return true + + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.Queueitem + + if flight.groupname==name then + + flight.nunits=flight.nunits-1 + + if flight.nunits==0 then + env.info(string.format("FF removing group %s from queue.", name)) + table.remove(queue, i) + end + end end - return false + end ---- Get player data from unit object --- @param #AIRBOSS self --- @param Wrapper.Unit#UNIT unit Unit in question. --- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. -function AIRBOSS:_GetPlayerDataUnit(unit) - if unit:IsAlive() then - local unitname=unit:GetName() - local playerunit,playername=self:_GetPlayerUnitAndName(unitname) - if playerunit and playername then - return self.players[playername] - end - end - return nil -end - - ---- Get player data from group object. --- @param #AIRBOSS self --- @param Wrapper.Group#GROUP group Group in question. --- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. -function AIRBOSS:_GetPlayerDataGroup(group) - local units=group:GetUnits() - for _,unit in pairs(units) do - local playerdata=self:_GetPlayerDataUnit(unit) - if playerdata then - return playerdata - end - end - return nil -end +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Status +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Check current player status. -- @param #AIRBOSS self @@ -1192,7 +1426,7 @@ function AIRBOSS:_CheckPlayerStatus() end -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). - if unit:IsInZone(self.carrierZone) then + if unit:IsInZone(self.zoneCCA) then -- Check if player was previously not inside the zone. if playerData.inbigzone==false then @@ -1201,8 +1435,8 @@ function AIRBOSS:_CheckPlayerStatus() local text=string.format("Welcome back, %s! TCN 74X, ICLS 1, BRC 354 (MAG HDG).\n", playerData.callsign) -- Heading and distance to register for approach. - local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) - local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) + local heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) + local distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitialzone:GetCoordinate()) -- Send message. text=text..string.format("Fly heading %d for %.1f NM and turn to BRC.", heading, distance) @@ -1440,6 +1674,7 @@ function AIRBOSS:OnEventLand(EventData) end + -- Remove if self:_InQueue(self.Qpattern, EventData.IniGroup) then self:_RemoveQueue(self.Qpattern, EventData.IniGroup) end @@ -1466,29 +1701,10 @@ function AIRBOSS:OnEventCrash(EventData) end end ---- Airboss event handler for event land. --- @param #AIRBOSS self --- @param #table queue The queue from which the group will be removed. --- @param Wrapper.Group#GROUP group Group that will be removed from queue. -function AIRBOSS:_RemoveQueue(queue, group) - local name=group:GetName() - - for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Queueitem - if flight.groupname==name then - flight.nunits=flight.nunits-1 - if flight.nunits==0 then - env.info(string.format("FF removing group %s from queue.", name)) - table.remove(queue, i) - end - end - end - -end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- AIRBOSS functions +-- PATTERN functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Initialize player data. @@ -1524,7 +1740,7 @@ function AIRBOSS:_InitPlayer(unitname) playerData.difficulty=playerData.difficulty or AIRBOSS.Difficulty.NORMAL -- Player is in the big zone around the carrier. - playerData.inbigzone=playerData.unit:IsInZone(self.carrierZone) + playerData.inbigzone=playerData.unit:IsInZone(self.zoneCCA) -- Init stuff for this round. playerData=self:_InitNewApproach(playerData) @@ -1564,7 +1780,7 @@ end function AIRBOSS:_Commencing(playerData) local text="Commencing." - self:_SendMessageToPlayer(text, 10, playerData) + -- Initialize player data for new approach. self:_InitNewApproach(playerData) @@ -1578,6 +1794,8 @@ function AIRBOSS:_Commencing(playerData) playerData.step=AIRBOSS.PatternStep.DESCENT4K end + -- Message to player. + self:_SendMessageToPlayer(text, 10, playerData) end --- Start pattern when player enters the initial zone. @@ -2266,29 +2484,7 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA) return waveoff end ---- Get name of the current pattern step. --- @param #AIRBOSS self --- @param #number step Step --- @return #string Name of the step -function AIRBOSS:_GS(step) - local gp - if step==90 then - gp="X0" -- Entering the groove. - elseif step==91 then - gp="X" -- Starting the groove. - elseif step==92 then - gp="RB" -- Roger ball call. - elseif step==93 then - gp="IM" -- In the middle. - elseif step==94 then - gp="IC" -- In close. - elseif step==95 then - gp="AR" -- At the ramp. - elseif step==96 then - gp="IW" -- In the wires. - end - return gp -end + --- Trapped? -- @param #AIRBOSS self @@ -2340,135 +2536,57 @@ function AIRBOSS:_Trapped(playerData, pos) playerData.step=AIRBOSS.PatternStep.DEBRIEF end ---- LSO advice call. +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ORIENTATION functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Provide info about player status on the fly. -- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. --- @param #number glideslopeError Error in degrees. --- @param #number lineupError Error in degrees. -function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_DetailedPlayerStatus(playerData) - -- Player group. - local player=playerData.unit:GetGroup() + -- Player unit. + local unit=playerData.unit - -- Init delay. - local delay=0 + -- Aircraft attitude. + local aoa=unit:GetAoA() + local yaw=unit:GetYaw() + local roll=unit:GetRoll() + local pitch=unit:GetPitch() - -- Glideslope high/low calls. - local text="" - if glideslopeError>1 then - --text="You're high!" - --AIRBOSS.LSOcall.HIGHL:ToGroup(player) - self:RadioTransmission(self.LSOradio, self.radiocall.HIGH, true, delay) - delay=delay+1.5 - elseif glideslopeError>0.5 then - --text="You're a little high." - --AIRBOSS.LSOcall.HIGHS:ToGroup(player) - self:RadioTransmission(self.LSOradio, self.radiocall.HIGH, false, delay) - delay=delay+1.5 - elseif glideslopeError<-1.0 then - --text="Power!" - --AIRBOSS.LSOcall.POWERL:ToGroup(player) - self:RadioTransmission(self.LSOradio, self.radiocall.POWER, true, delay) - delay=delay+1.5 - elseif glideslopeError<-0.5 then - --text="You're a little low." - --AIRBOSS.LSOcall.POWERS:ToGroup(player) - self:RadioTransmission(self.LSOradio, self.radiocall.POWER, false, delay) - delay=delay+1.5 - else - text="Good altitude." - end + -- Distance to the boat. + local dist=playerData.unit:GetCoordinate():Get2DDistance(self.carrier:GetCoordinate()) + local dx,dz,rho,phi=self:_GetDistances(unit) - text=text..string.format(" Glideslope Error = %.2f°", glideslopeError) - text=text.."\n" + -- Wind vector. + local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() - -- Lineup left/right calls. - if lineupError<-3 then - --text=text.."Come left!" - --AIRBOSS.LSOcall.COMELEFTL:ToGroup(player, delay) - self:RadioTransmission(self.LSOradio, self.radiocall.COMELEFT, true, delay) - delay=delay+1.5 - elseif lineupError<-1 then - --text=text.."Come left." - --AIRBOSS.LSOcall.COMELEFTS:ToGroup(player, delay) - self:RadioTransmission(self.LSOradio, self.radiocall.COMELEFT, false, delay) - delay=delay+1.5 - elseif lineupError>3 then - --text=text.."Right for lineup!" - --AIRBOSS.LSOcall.RIGHTFORLINEUPL:ToGroup(player, delay) - self:RadioTransmission(self.LSOradio, self.radiocall.RIGHTFORLINEUP, true, delay) - delay=delay+1.5 - elseif lineupError>1 then - --text=text.."Right for lineup." - --AIRBOSS.LSOcall.RIGHTFORLINEUPS:ToGroup(player, delay) - self:RadioTransmission(self.LSOradio, self.radiocall.RIGHTFORLINEUP, false, delay) - delay=delay+1.5 - else - text=text.."Good lineup." + -- Aircraft veloecity vector. + local velo=unit:GetVelocityVec3() + + -- Relative heading Aircraft to Carrier. + local relhead=self:_GetRelativeHeading(playerData.unit) + + -- Output + local text=string.format("AoA=%.1f | Vx=%.1f Vy=%.1f Vz=%.1f\n", aoa, velo.x, velo.y, velo.z) + text=text..string.format("Pitch=%.1f° | Roll=%.1f° | Yaw=%.1f° | Climb=%.1f°\n", pitch, roll, yaw, unit:GetClimbAngle()) + text=text..string.format("Relheading=%.1f°\n", relhead) + text=text..string.format("Distance: X=%d m Z=%d m | R=%d m Phi=%.1f\n", dx, dz, rho, phi) + --TODO: step names for check if in groove + --[[ + if playerData.step>=90 and playerData.step<=99 then + local lineup=self:_Lineup(playerData)-self.carrierparam.rwyangle + local glideslope=self:_Glideslope(playerData)-3.5 + text=text..string.format("Lineup Error = %.1f°\n", lineup) + text=text..string.format("Glideslope Error = %.1f°\n", glideslope) end + ]] + text=text..string.format("Current step: %s\n", playerData.step) - text=text..string.format(" Lineup Error = %.1f°\n", lineupError) - - -- Get AoA. - local aoa=playerData.unit:GetAoA() - - if aoa>=9.3 then - --text=text.."Your're slow!" - self:RadioTransmission(self.LSOradio, self.radiocall.SLOW, true, delay) - delay=delay+1.5 - elseif aoa>=8.8 and aoa<9.3 then - self:RadioTransmission(self.LSOradio, self.radiocall.SLOW, false, delay) - delay=delay+1.5 - --text=text.."Your're a little slow." - elseif aoa>=7.4 and aoa<8.8 then - text=text.."You're on speed." - elseif aoa>=6.9 and aoa<7.4 then - --text=text.."You're a little fast." - self:RadioTransmission(self.LSOradio, self.radiocall.FAST, false, delay) - delay=delay+1.5 - elseif aoa>=0 and aoa<6.9 then - text=text.."You're fast!" - self:RadioTransmission(self.LSOradio, self.radiocall.FALSE, true, delay) - delay=delay+1.5 - else - text=text.."Unknown AoA state." - end - - text=text..string.format(" AoA = %.1f", aoa) - - -- LSO Message to player. - self:_SendMessageToPlayer(text, 5, playerData, false) - - -- Set last time. - playerData.Tlso=timer.getTime() -end + --text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) + --text=text..string.format("rho=%.1f m phi=%.1f degrees\n", rho,phi) ---- Radio transmission. --- @param #AIRBOSS self --- @param Core.Radio#RADIO radio sending transmission. --- @param #AIRBOSS.RadioSound call Radio sound files and subtitles. --- @param #boolean loud If true, play loud sound file version. --- @param #number delay Delay in seconds, before the message is broadcasted. -function AIRBOSS:RadioTransmission(radio, call, loud, delay) - - if delay==nil or delay and delay==0 then - - local filename=call.normal - if loud then - filename=call.loud - end - - -- New transmission. - radio:NewUnitTransmission(filename, call.subtitle, call.duration, radio.Frequency, radio.Modulation, false) - - -- Broadcast message. - radio:Broadcast() - - else - - -- Scheduled transmission. - SCHEDULER:New(nil, self.RadioCall, {self, radio, call, loud}, delay) - end + MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) end --- Get glide slope of aircraft. @@ -2577,76 +2695,6 @@ function AIRBOSS:_Radial() return radial end - ---------- --- Bla functions ---------- - ---- Append text to debrief text. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data. --- @param #string step Current step in the pattern. --- @param #string item Text item appeded to the debrief. -function AIRBOSS:_AddToSummary(playerData, step, item) - table.insert(playerData.debrief, {step=step, hint=item}) -end - ---- Show debriefing message. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_Debrief(playerData) - env.info("FF debrief") - - -- Debriefing text. - local text=string.format("Debriefing:\n") - text=text..string.format("================================\n") - for _,_data in pairs(playerData.debrief) do - local step=_data.step - local comment=_data.hint - text=text..string.format("* %s:\n",step) - text=text..string.format("%s\n", comment) - end - - -- Send debrief message to player - self:_SendMessageToPlayer(text, 30, playerData, true, "Paddles") - - -- LSO grade, points, and flight data analyis. - local grade, points, analysis=self:_LSOgrade(playerData) - - local mygrade={} --#AIRBOSS.LSOgrade - mygrade.grade=grade - mygrade.points=points - mygrade.details=analysis - - -- Add grade to table. - table.insert(playerData.grades, mygrade) - - -- LSO grade message. - text=string.format("%s %.1f PT - %s", grade, points, analysis) - self:_SendMessageToPlayer(text, 10, playerData, true, "Paddles", 30) - - -- New approach. - if playerData.boltered or playerData.waveoff or playerData.patternwo then - - -- Get heading and distance to register zone ~3 NM astern. - local heading=playerData.unit:GetCoordinate():HeadingTo(self.registerZone:GetCoordinate()) - local distance=playerData.unit:GetCoordinate():Get2DDistance(self.registerZone:GetCoordinate()) - - local text=string.format("fly heading %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) - self:_SendMessageToPlayer(text, 10, playerData, false, nil, 30) - - -- Next step? - -- TODO: CASE I: After bolter/wo turn left and climb to 600 ft and re-enter the pattern. But do not go to initial but reenter earlier? - -- TODO: CASE I: After pattern wo? go back to initial, I guess? - -- TODO: CASE III: After bolter/wo turn left and climb to 1200 ft and re-enter pattern? - -- TODO: CASE III: After pattern wo? No idea... - playerData.step=AIRBOSS.PatternStep.COMMENCING - end - - -- Next step. - playerData.step=AIRBOSS.PatternStep.UNDEFINED -end - --- Get relative heading of player wrt carrier. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Player unit. @@ -2709,332 +2757,6 @@ function AIRBOSS:_GetDistances(unit) return dx,dz,rho,phi end ---- Check if a player is within the right area. --- @param #AIRBOSS self --- @param #number X X distance player to carrier. --- @param #number Z Z distance player to carrier. --- @param #AIRBOSS.Checkpoint pos Position data limits. --- @return #boolean If true, approach should be aborted. -function AIRBOSS:_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 #AIRBOSS self --- @param #number X X distance player to carrier. --- @param #number Z Z distance player to carrier. --- @param #AIRBOSS.Checkpoint posData Checkpoint data. -function AIRBOSS:_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 #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data. --- @param #number X X distance player to carrier. --- @param #number Z Z distance player to carrier. --- @param #AIRBOSS.Checkpoint posData Checkpoint data. -function AIRBOSS:_AbortPattern(playerData, X, Z, posData) - - -- Text where we are wrong. - local toofartext=self:_TooFarOutText(X, Z, posData) - - -- Send message to player. - self:_SendMessageToPlayer(toofartext.." Depart and re-enter!", 15, playerData, true) - - -- 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) - - -- Add to debrief. - self:_AddToSummary(playerData, string.format("%s", playerData.step), string.format("Pattern wave off: %s", toofartext)) - - -- Pattern wave off! - playerData.patternwo=true - - -- Next step debrief. - playerData.step=AIRBOSS.PatternStep.DEBRIEF -end - - ---- Provide info about player status on the fly. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_DetailedPlayerStatus(playerData) - - -- Player unit. - local unit=playerData.unit - - -- Aircraft attitude. - local aoa=unit:GetAoA() - local yaw=unit:GetYaw() - local roll=unit:GetRoll() - local pitch=unit:GetPitch() - - -- Distance to the boat. - local dist=playerData.unit:GetCoordinate():Get2DDistance(self.carrier:GetCoordinate()) - local dx,dz,rho,phi=self:_GetDistances(unit) - - -- Wind vector. - local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() - - -- Aircraft veloecity vector. - local velo=unit:GetVelocityVec3() - - -- Relative heading Aircraft to Carrier. - local relhead=self:_GetRelativeHeading(playerData.unit) - - -- Output - local text=string.format("AoA=%.1f | Vx=%.1f Vy=%.1f Vz=%.1f\n", aoa, velo.x, velo.y, velo.z) - text=text..string.format("Pitch=%.1f° | Roll=%.1f° | Yaw=%.1f° | Climb=%.1f°\n", pitch, roll, yaw, unit:GetClimbAngle()) - text=text..string.format("Relheading=%.1f°\n", relhead) - text=text..string.format("Distance: X=%d m Z=%d m | R=%d m Phi=%.1f\n", dx, dz, rho, phi) - if playerData.step>=90 and playerData.step<=99 then - local lineup=self:_Lineup(playerData)-self.rwyangle - local glideslope=self:_Glideslope(playerData)-3.5 - text=text..string.format("Lineup Error = %.1f°\n", lineup) - text=text..string.format("Glideslope Error = %.1f°\n", glideslope) - end - text=text..string.format("Current step: %s\n", playerData.step) - - --text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) - --text=text..string.format("rho=%.1f m phi=%.1f degrees\n", rho,phi) - - MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) -end - ---- Init parameters for USS Stennis carrier. --- @param #AIRBOSS self -function AIRBOSS:_InitStennis() - - -- Carrier Parameters. - self.carrierparam.rwyangle = -9 - self.carrierparam.sterndist =-150 - self.carrierparam.deckheight = 22 - self.carrierparam.wire1 =-104 - self.carrierparam.wire2 = -92 - self.carrierparam.wire3 = -80 - self.carrierparam.wire4 = -68 - - --[[ - q0=self.carrier:GetCoordinate():SetAltitude(25) - q0:BigSmokeSmall(0.1) - q1=self.carrier:GetCoordinate():Translate(-104,0):SetAltitude(22) --1st wire - q1:BigSmokeSmall(0.1)--:SmokeGreen() - q2=self.carrier:GetCoordinate():Translate(-68,0):SetAltitude(22) --4th wire ==> distance between wires 12 m - q2:BigSmokeSmall(0.1)--:SmokeBlue() - ]] - - -- 4k descent from holding pattern to 5k platform - self.C3Descent4k.name="4k Descent" - self.C3Descent4k.Xmin=-UTILS.NMToMeters(35) - self.C3Descent4k.Xmax=-UTILS.NMToMeters(20) - self.C3Descent4k.Zmin=-UTILS.NMToMeters(30) - self.C3Descent4k.Zmax= UTILS.NMToMeters(30) - self.C3Descent4k.LimitXmin=nil - self.C3Descent4k.LimitXmax=-UTILS.NMToMeters(20) --TODO: better rho dist. decrease descent 20 2000 ft/min at 5000 ft alt and user rad alt. - self.C3Descent4k.LimitZmin=nil - self.C3Descent4k.LimitZmax=nil - self.C3Descent4k.Altitude=nil --UTILS.FeetToMeters(5000) - self.C3Descent4k.AoA=nil - self.C3Descent4k.Distance=nil - - -- 2k descent from 5k platform to 1200 dirty up level flight. - self.C3Descent2k.name="2k Descent" - self.C3Descent2k.Xmin=-UTILS.NMToMeters(21) - self.C3Descent2k.Xmax=nil - self.C3Descent2k.Zmin=-UTILS.NMToMeters(30) - self.C3Descent2k.Zmax= UTILS.NMToMeters(30) - self.C3Descent2k.LimitXmin=nil - self.C3Descent2k.LimitXmax=-UTILS.NMToMeters(12) --TODO: better rho dist! now switch to dirty up level flight 12 NM. - self.C3Descent2k.LimitZmin=nil - self.C3Descent2k.LimitZmax=nil - self.C3Descent2k.Altitude=UTILS.FeetToMeters(5000) - self.C3Descent2k.AoA=nil - self.C3Descent2k.Distance=-UTILS.NMToMeters(20) - - -- Level out at 1200 ft and dirty up. - self.C3DirtyUp.name="Dirty Up" - self.C3DirtyUp.Xmin=-UTILS.NMToMeters(13) - self.C3DirtyUp.Xmax=nil - self.C3DirtyUp.Zmin=-UTILS.NMToMeters(30) - self.C3DirtyUp.Zmax= UTILS.NMToMeters(30) - self.C3DirtyUp.LimitXmin=nil - self.C3DirtyUp.LimitXmax=-UTILS.NMToMeters(3) --TODO: better rho dist! Intercept glideslope and follow bullseye. - self.C3DirtyUp.LimitZmin=nil - self.C3DirtyUp.LimitZmax=nil - self.C3DirtyUp.Altitude=UTILS.FeetToMeters(1200) - self.C3DirtyUp.AoA=nil - self.C3DirtyUp.Distance=-UTILS.NMToMeters(12) - - -- Intercept glide slope and follow bullseye. - self.C3DirtyUp.name="Bullseye" - self.C3DirtyUp.Xmin=-UTILS.NMToMeters(4) - self.C3DirtyUp.Xmax=nil - self.C3DirtyUp.Zmin=-UTILS.NMToMeters(30) - self.C3DirtyUp.Zmax= UTILS.NMToMeters(30) - self.C3DirtyUp.LimitXmin=nil - self.C3DirtyUp.LimitXmax=-UTILS.NMToMeters(1) --TODO: better rho dist! Call the ball. - self.C3DirtyUp.LimitZmin=nil - self.C3DirtyUp.LimitZmax=nil - self.C3DirtyUp.Altitude=UTILS.FeetToMeters(1200) - self.C3DirtyUp.AoA=nil - self.C3DirtyUp.Distance=-UTILS.NMToMeters(3) - - -- Upwind leg - self.Upwind.name="Upwind" - self.Upwind.Xmin=-UTILS.NMToMeters(4) - self.Upwind.Xmax=nil - self.Upwind.Zmin=0 - self.Upwind.Zmax=1000 - 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=UTILS.NMToMeters(5) - 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=UTILS.NMToMeters(5) - 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=-1000 - 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 #AIRBOSS self -- @param #number X X position of player unit. @@ -3063,9 +2785,112 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- MISC functions +-- LSO functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- LSO advice radio call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number glideslopeError Error in degrees. +-- @param #number lineupError Error in degrees. +function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) + + -- Player group. + local player=playerData.unit:GetGroup() + + -- Init delay. + local delay=0 + + -- Glideslope high/low calls. + local text="" + if glideslopeError>1 then + --text="You're high!" + --AIRBOSS.LSOcall.HIGHL:ToGroup(player) + self:RadioTransmission(self.LSOradio, self.radiocall.HIGH, true, delay) + delay=delay+1.5 + elseif glideslopeError>0.5 then + --text="You're a little high." + --AIRBOSS.LSOcall.HIGHS:ToGroup(player) + self:RadioTransmission(self.LSOradio, self.radiocall.HIGH, false, delay) + delay=delay+1.5 + elseif glideslopeError<-1.0 then + --text="Power!" + --AIRBOSS.LSOcall.POWERL:ToGroup(player) + self:RadioTransmission(self.LSOradio, self.radiocall.POWER, true, delay) + delay=delay+1.5 + elseif glideslopeError<-0.5 then + --text="You're a little low." + --AIRBOSS.LSOcall.POWERS:ToGroup(player) + self:RadioTransmission(self.LSOradio, self.radiocall.POWER, false, delay) + delay=delay+1.5 + else + text="Good altitude." + end + + text=text..string.format(" Glideslope Error = %.2f°", glideslopeError) + text=text.."\n" + + -- Lineup left/right calls. + if lineupError<-3 then + --text=text.."Come left!" + --AIRBOSS.LSOcall.COMELEFTL:ToGroup(player, delay) + self:RadioTransmission(self.LSOradio, self.radiocall.COMELEFT, true, delay) + delay=delay+1.5 + elseif lineupError<-1 then + --text=text.."Come left." + --AIRBOSS.LSOcall.COMELEFTS:ToGroup(player, delay) + self:RadioTransmission(self.LSOradio, self.radiocall.COMELEFT, false, delay) + delay=delay+1.5 + elseif lineupError>3 then + --text=text.."Right for lineup!" + --AIRBOSS.LSOcall.RIGHTFORLINEUPL:ToGroup(player, delay) + self:RadioTransmission(self.LSOradio, self.radiocall.RIGHTFORLINEUP, true, delay) + delay=delay+1.5 + elseif lineupError>1 then + --text=text.."Right for lineup." + --AIRBOSS.LSOcall.RIGHTFORLINEUPS:ToGroup(player, delay) + self:RadioTransmission(self.LSOradio, self.radiocall.RIGHTFORLINEUP, false, delay) + delay=delay+1.5 + else + text=text.."Good lineup." + end + + text=text..string.format(" Lineup Error = %.1f°\n", lineupError) + + -- Get AoA. + local aoa=playerData.unit:GetAoA() + + if aoa>=9.3 then + --text=text.."Your're slow!" + self:RadioTransmission(self.LSOradio, self.radiocall.SLOW, true, delay) + delay=delay+1.5 + elseif aoa>=8.8 and aoa<9.3 then + self:RadioTransmission(self.LSOradio, self.radiocall.SLOW, false, delay) + delay=delay+1.5 + --text=text.."Your're a little slow." + elseif aoa>=7.4 and aoa<8.8 then + text=text.."You're on speed." + elseif aoa>=6.9 and aoa<7.4 then + --text=text.."You're a little fast." + self:RadioTransmission(self.LSOradio, self.radiocall.FAST, false, delay) + delay=delay+1.5 + elseif aoa>=0 and aoa<6.9 then + text=text.."You're fast!" + self:RadioTransmission(self.LSOradio, self.radiocall.FALSE, true, delay) + delay=delay+1.5 + else + text=text.."Unknown AoA state." + end + + text=text..string.format(" AoA = %.1f", aoa) + + -- LSO Message to player. + self:_SendMessageToPlayer(text, 5, playerData, false) + + -- Set last time. + playerData.Tlso=timer.getTime() +end + --- Grade approach. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -3258,6 +3083,118 @@ function AIRBOSS:_Flightdata2Text(fdata) return G,n end +--- Get short name of the grove step. +-- @param #AIRBOSS self +-- @param #number step Step +-- @return #string Shortcut name "X", "RB", "IM", "AR", "IW". +function AIRBOSS:_GS(step) + local gp + if step==AIRBOSS.PatternStep.FINAL then + gp="X0" -- Entering the groove. + elseif step==AIRBOSS.PatternStep then + gp="X" -- Starting the groove. + elseif step==AIRBOSS.PatternStep.GROOVE_RB then + gp="RB" -- Roger ball call. + elseif step==AIRBOSS.PatternStep.GROOVE_IM then + gp="IM" -- In the middle. + elseif step==AIRBOSS.PatternStep.GROOVE_IC then + gp="IC" -- In close. + elseif step==AIRBOSS.PatternStep.GROOVE_AR then + gp="AR" -- At the ramp. + elseif step==AIRBOSS.PatternStep.GROOVE_IW then + gp="IW" -- In the wires. + end + return gp +end + +--- Check if a player is within the right area. +-- @param #AIRBOSS self +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint pos Position data limits. +-- @return #boolean If true, approach should be aborted. +function AIRBOSS:_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 #AIRBOSS self +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint posData Checkpoint data. +function AIRBOSS:_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 #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint posData Checkpoint data. +function AIRBOSS:_AbortPattern(playerData, X, Z, posData) + + -- Text where we are wrong. + local toofartext=self:_TooFarOutText(X, Z, posData) + + -- Send message to player. + self:_SendMessageToPlayer(toofartext.." Depart and re-enter!", 15, playerData, true) + + -- 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) + + -- Add to debrief. + self:_AddToSummary(playerData, string.format("%s", playerData.step), string.format("Pattern wave off: %s", toofartext)) + + -- Pattern wave off! + playerData.patternwo=true + + -- Next step debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF +end + + --- Evaluate player's altitude at checkpoint. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -3425,6 +3362,102 @@ function AIRBOSS:_AoACheck(playerData, checkpoint, aoa) return hint, debrief end +--- Append text to debrief text. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step Current step in the pattern. +-- @param #string item Text item appeded to the debrief. +function AIRBOSS:_AddToSummary(playerData, step, item) + table.insert(playerData.debrief, {step=step, hint=item}) +end + +--- Show debriefing message. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Debrief(playerData) + env.info("FF debrief") + + -- Debriefing text. + local text=string.format("Debriefing:\n") + text=text..string.format("================================\n") + for _,_data in pairs(playerData.debrief) do + local step=_data.step + local comment=_data.hint + text=text..string.format("* %s:\n",step) + text=text..string.format("%s\n", comment) + end + + -- Send debrief message to player + self:_SendMessageToPlayer(text, 30, playerData, true, "Paddles") + + -- LSO grade, points, and flight data analyis. + local grade, points, analysis=self:_LSOgrade(playerData) + + local mygrade={} --#AIRBOSS.LSOgrade + mygrade.grade=grade + mygrade.points=points + mygrade.details=analysis + + -- Add grade to table. + table.insert(playerData.grades, mygrade) + + -- LSO grade message. + text=string.format("%s %.1f PT - %s", grade, points, analysis) + self:_SendMessageToPlayer(text, 10, playerData, true, "Paddles", 30) + + -- New approach. + if playerData.boltered or playerData.waveoff or playerData.patternwo then + + -- Get heading and distance to register zone ~3 NM astern. + local heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) + local distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) + + local text=string.format("fly heading %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) + self:_SendMessageToPlayer(text, 10, playerData, false, nil, 30) + + -- Next step? + -- TODO: CASE I: After bolter/wo turn left and climb to 600 ft and re-enter the pattern. But do not go to initial but reenter earlier? + -- TODO: CASE I: After pattern wo? go back to initial, I guess? + -- TODO: CASE III: After bolter/wo turn left and climb to 1200 ft and re-enter pattern? + -- TODO: CASE III: After pattern wo? No idea... + playerData.step=AIRBOSS.PatternStep.COMMENCING + end + + -- Next step. + playerData.step=AIRBOSS.PatternStep.UNDEFINED +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MISC functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Radio transmission. +-- @param #AIRBOSS self +-- @param Core.Radio#RADIO radio sending transmission. +-- @param #AIRBOSS.RadioSound call Radio sound files and subtitles. +-- @param #boolean loud If true, play loud sound file version. +-- @param #number delay Delay in seconds, before the message is broadcasted. +function AIRBOSS:RadioTransmission(radio, call, loud, delay) + + if delay==nil or delay and delay==0 then + + local filename=call.normal + if loud then + filename=call.loud + end + + -- New transmission. + radio:NewUnitTransmission(filename, call.subtitle, call.duration, radio.Frequency, radio.Modulation, false) + + -- Broadcast message. + radio:Broadcast() + + else + + -- Scheduled transmission. + SCHEDULER:New(nil, self.RadioTransmission, {self, radio, call, loud}, delay) + end +end --- Send message to playe client. -- @param #AIRBOSS self @@ -3457,6 +3490,71 @@ function AIRBOSS:_SendMessageToPlayer(message, duration, playerData, clear, send end +--- Checks if a group has a human player. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #boolean If true, human player inside group. +function AIRBOSS:_IsHuman(group) + + local units=group:GetUnits() + + for _,_unit in pairs(units) do + local playerunit=self:_GetPlayerUnitAndName(_unit:GetName()) + if playerunit then + return true + end + end + + return false +end + +--- Check if a group is in the queue. +-- @param #AIRBOSS self +-- @param #table queue The queue to check. +-- @param Wrapper.Group#GROUP group +-- @return #boolean If true, group is in the queue. False otherwise. +function AIRBOSS:_InQueue(queue, group) + local name=group:GetName() + for _,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.Queueitem + if name==flight.groupname then + return true + end + end + return false +end + +--- Get player data from unit object +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Unit in question. +-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. +function AIRBOSS:_GetPlayerDataUnit(unit) + if unit:IsAlive() then + local unitname=unit:GetName() + local playerunit,playername=self:_GetPlayerUnitAndName(unitname) + if playerunit and playername then + return self.players[playername] + end + end + return nil +end + + +--- Get player data from group object. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Group in question. +-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. +function AIRBOSS:_GetPlayerDataGroup(group) + local units=group:GetUnits() + for _,unit in pairs(units) do + local playerdata=self:_GetPlayerDataUnit(unit) + if playerdata then + return playerdata + end + end + return nil +end + --- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. -- @param #AIRBOSS self -- @param #string _unitName Name of the player unit. @@ -3524,29 +3622,37 @@ function AIRBOSS:_AddF10Commands(_unitName) local playerData=self.players[playername] -- F10/Airboss/ - local _trainPath = missionCommands.addSubMenuForGroup(_gid, self.alias, AIRBOSS.MenuF10[_gid]) + local _rootPath = missionCommands.addSubMenuForGroup(_gid, self.alias, AIRBOSS.MenuF10[_gid]) -- F10/Airboss//Results - local _statsPath = missionCommands.addSubMenuForGroup(_gid, "LSO Grades", _trainPath) + local _statsPath = missionCommands.addSubMenuForGroup(_gid, "LSO Grades", _rootPath) - -- F10/Airboss//My Settings/Difficulty - local _difficulPath = missionCommands.addSubMenuForGroup(_gid, "Difficulty", _trainPath) + -- F10/Airboss//My Settings/Skil Level + local _skillPath = missionCommands.addSubMenuForGroup(_gid, "Skill Level", _rootPath) - -- F10/Airboss//Results/ - missionCommands.addCommandForGroup(_gid, "Greenie Board", _statsPath, self._DisplayScoreBoard, self, _unitName) + -- F10/Airboss//My Settings/Kneeboard + --local _kneeboardPath = missionCommands.addSubMenuForGroup(_gid, "Kneeboard", _rootPath) + + -- F10/Airboss//LSO Grades/ + missionCommands.addCommandForGroup(_gid, "Greenie Board", _statsPath, self._DisplayScoreBoard, self, _unitName) missionCommands.addCommandForGroup(_gid, "My Grades", _statsPath, self._DisplayPlayerGrades, self, _unitName) --missionCommands.addCommandForGroup(_gid, "(Clear ALL Results)", _statsPath, self._ResetRangeStats, self, _unitName) -- F10/Airboss//Difficulty - missionCommands.addCommandForGroup(_gid, "Flight Student", _difficulPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) - missionCommands.addCommandForGroup(_gid, "Naval Aviator", _difficulPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) - missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _difficulPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) + missionCommands.addCommandForGroup(_gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) + missionCommands.addCommandForGroup(_gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) + missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) + missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _skillPath, self._AttitudeMonitor, self, playername) -- F10/Airboss// - missionCommands.addCommandForGroup(_gid, "Carrier Info", _trainPath, self._DisplayCarrierInfo, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Weather Report", _trainPath, self._DisplayCarrierWeather, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _trainPath, self._AttitudeMonitor, self, playername) - --TODO: Flare carrier. + missionCommands.addCommandForGroup(_gid, "Weather Report", _rootPath, self._DisplayCarrierWeather, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Carrier Info", _rootPath, self._DisplayCarrierInfo, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Request Straight-In", _rootPath, self._RequestStraight, self, _unitName) + + -- TODO: request straight in approach + -- TODO: request refuelling. + -- end @@ -3559,6 +3665,25 @@ function AIRBOSS:_AddF10Commands(_unitName) end +--- Request marshal. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestMarshal(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + self:_MarshalPlayer(_unit:GetGroup()) + end + end +end + --- Display top 10 player scores. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. From 9b40ebe00cfc4bd0117a2127406a58f7cc88c467 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 15 Nov 2018 23:16:58 +0100 Subject: [PATCH 044/147] AIRBOSS v0.2.7 --- .../Moose/Functional/CarrierTrainer.lua | 83 +++++++++++++++---- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Functional/CarrierTrainer.lua index 739bd1039..2230e6993 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Functional/CarrierTrainer.lua @@ -381,7 +381,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.2.6w" +AIRBOSS.version="0.2.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -449,12 +449,14 @@ function AIRBOSS:New(carriername, alias) self.beacon=BEACON:New(self.carrier) -- Set up Airboss radio. - self.Carrierradio=RADIO:New(self.carrier) self:SetCarrierradio() + self.Carrierradio=RADIO:New(self.carrier) + self.Carrierradio:SetFrequency(self.Carrierfreq) -- Set up LSO radio. - self.LSOradio=RADIO:New(self.carrier) self:SetLSOradio() + self.LSOradio=RADIO:New(self.carrier) + self.LSOradio:SetFrequency(self.LSOfreq) -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then @@ -709,7 +711,7 @@ function AIRBOSS:onafterStart(From, Event, To) -- Handle events. self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Land) - --self:HandleEvent(EVENTS.Crash) + self:HandleEvent(EVENTS.Crash) -- Time stamp for checking queues. self.Tqueue=timer.getTime() @@ -819,6 +821,7 @@ end function AIRBOSS:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Birth) self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.Crash) end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1136,10 +1139,12 @@ function AIRBOSS:_ScanCarrierZone() -- Check that it is not already in one of the queues. if not (self:_InQueue(self.Qmarshal, group) or self:_InQueue(self.Qpattern, group)) then - env.info("FF new marshal group="..groupname) + if self:_IsHuman(group) then - self:_MarshalPlayer(group) + env.info("FF new HUMAN marshal group (not used, register manually!)="..groupname) + --self:_MarshalPlayer(group) else + env.info("FF new AI marshal group="..groupname) self:_MarshalAI(group) end end @@ -1165,10 +1170,12 @@ end -- @param Wrapper.Group#GROUP group Aircraft group. -- @param #number flagvalue Initial user flag value. -- @param #number alt Altitude in feet. -function AIRBOSS:_AddFlightGroup(group) +-- @return #AIRBOSS.Queueitem Flight group. +function AIRBOSS:_CreateFlightGroup(group) -- Flight group name local groupname=group:GetName() + local human=self:_IsHuman(group) -- Queue table item. local qitem={} --#AIRBOSS.Queueitem @@ -1178,14 +1185,15 @@ function AIRBOSS:_AddFlightGroup(group) qitem.fuel=group:GetFuelMin() qitem.time=timer.getTime() qitem.flag=USERFLAG:New(groupname) - qitem.flag:Set(-100) - qitem.ai=not self:_IsHuman(group) + qitem.flag:Set(-100) + qitem.ai=not human if human then local playerData=self:_GetPlayerDataGroup(group) qitem.player=playerData end + end --- Orbit at a specified position at a specified alititude with a specified speed. @@ -1375,6 +1383,25 @@ function AIRBOSS:_CollapseMarshalStack() table.remove(self.Qmarshal, 1) end +--- Remove a group from a queue. +-- @param #AIRBOSS self +-- @param #table queue The queue from which the group will be removed. +-- @param Wrapper.Group#GROUP group Group that will be removed from queue. +function AIRBOSS:_RemoveGroupFromQueue(queue, group) + + local name=group:GetName() + + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.Queueitem + + if flight.groupname==name then + env.info(string.format("FF removing group %s from queue.", name)) + table.remove(queue, i) + end + end + +end + --- Remove a group from a queue. -- @param #AIRBOSS self -- @param #table queue The queue from which the group will be removed. @@ -1449,7 +1476,7 @@ function AIRBOSS:_CheckPlayerStatus() self:I("Player status undefined. Waiting for next step.") -- Jump directly to CASE I straight in approach. - playerData.step=AIRBOSS.PatternStep.COMMENCING + --playerData.step=AIRBOSS.PatternStep.COMMENCING -- Jump to final/groove for testing. if self.groovedebug then @@ -3091,7 +3118,7 @@ function AIRBOSS:_GS(step) local gp if step==AIRBOSS.PatternStep.FINAL then gp="X0" -- Entering the groove. - elseif step==AIRBOSS.PatternStep then + elseif step==AIRBOSS.PatternStep.GROOVE_XX then gp="X" -- Starting the groove. elseif step==AIRBOSS.PatternStep.GROOVE_RB then gp="RB" -- Roger ball call. @@ -3438,6 +3465,9 @@ end -- @param #boolean loud If true, play loud sound file version. -- @param #number delay Delay in seconds, before the message is broadcasted. function AIRBOSS:RadioTransmission(radio, call, loud, delay) + self:F({radio=radio, call=call, loud=loud, delay=delay}) + + env.info("FF call = "..tostring(call)) if delay==nil or delay and delay==0 then @@ -3447,7 +3477,7 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay) end -- New transmission. - radio:NewUnitTransmission(filename, call.subtitle, call.duration, radio.Frequency, radio.Modulation, false) + radio:NewUnitTransmission(filename, call.subtitle, call.duration, radio.Frequency/1000000, radio.Modulation, false) -- Broadcast message. radio:Broadcast() @@ -3648,7 +3678,7 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "Weather Report", _rootPath, self._DisplayCarrierWeather, self, _unitName) missionCommands.addCommandForGroup(_gid, "Carrier Info", _rootPath, self._DisplayCarrierInfo, self, _unitName) missionCommands.addCommandForGroup(_gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Request Straight-In", _rootPath, self._RequestStraight, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Request Straight-In", _rootPath, self._RequestStraightIn, self, _unitName) -- TODO: request straight in approach -- TODO: request refuelling. @@ -3665,6 +3695,25 @@ function AIRBOSS:_AddF10Commands(_unitName) end +--- Request straight in approach. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestStraightIn(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + self:_MarshalPlayer(_unit:GetGroup()) + end + end +end + --- Request marshal. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. @@ -3854,9 +3903,13 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) text=text..string.format("Case %d Recovery\n", self.case) text=text..string.format("BRC %d°\n", self:_BaseRecoveryCourse()) text=text..string.format("FB %d°\n", self:_FinalBearing()) - text=text..string.format("Speed %d kts\n", carrierspeed) + text=text..string.format("Speed %d kts\n", carrierspeed) + text=text..string.format("Airboss radio %.3f MHz AM\n", self.Carrierfreq) --TODO: add modulation + text=text..string.format("LSO radio %.3f MHz AM\n", self.LSOfreq) text=text..string.format("TACAN Channel %s\n", tacan) - text=text..string.format("ICLS Channel %s", icls) + text=text..string.format("ICLS Channel %s\n", icls) + text=text..string.format("# A/C holding %d\n", #self.Qmarshal) + text=text..string.format("# A/C pattern %d", #self.Qpattern) -- Send message. self:_SendMessageToPlayer(text, 20, playerData) From 9ad948632c7af316eb6d2a46b9c8a00e9dc8f830 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 16 Nov 2018 16:24:01 +0100 Subject: [PATCH 045/147] AIRBOSS v0.2.7w --- .../CarrierTrainer.lua => Ops/Airboss.lua} | 1257 +++-------------- .../Moose/Ops/RecoveryTanker.lua | 558 ++++++++ Moose Development/Moose/Ops/RescueHelo.lua | 432 ++++++ 3 files changed, 1183 insertions(+), 1064 deletions(-) rename Moose Development/Moose/{Functional/CarrierTrainer.lua => Ops/Airboss.lua} (79%) create mode 100644 Moose Development/Moose/Ops/RecoveryTanker.lua create mode 100644 Moose Development/Moose/Ops/RescueHelo.lua diff --git a/Moose Development/Moose/Functional/CarrierTrainer.lua b/Moose Development/Moose/Ops/Airboss.lua similarity index 79% rename from Moose Development/Moose/Functional/CarrierTrainer.lua rename to Moose Development/Moose/Ops/Airboss.lua index 2230e6993..85eee83cc 100644 --- a/Moose Development/Moose/Functional/CarrierTrainer.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1,6 +1,6 @@ --- **Functional** - (R2.5) - Manages aircraft operations on carriers. -- --- The Moose AIRBOSS class manages recoveries of human pilots and AI aircraft for aircraft carriers. +-- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers. -- -- Features: -- @@ -9,8 +9,8 @@ -- * Automatic LSO grading. -- * Different skill level supporting tipps during for students or complete zip lip for pros. -- * Rescue helo option. --- * Overhead refuelling tanker option. --- * Voice overs for LSO and Airobss calls. Can easily customized by users. +-- * Recovery tanker option. +-- * Voice overs for LSO and AIRBOSS calls. Can easily customized by users. -- * Automatic TACAN and ICLS channel setting. -- * Different radio channels for LSO and airboss calls. -- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels, pilot grades). @@ -22,9 +22,10 @@ -- -- === -- --- ### Authors: **funkyfranky**, **Bankler** (Carrier trainer idea and script) +-- ### Author: **funkyfranky** +-- ### Co-author: **Bankler** (Carrier trainer idea and script) -- --- @module Functional.Airboss +-- @module Ops.Airboss -- @image MOOSE.JPG --- AIRBOSS class. @@ -63,18 +64,19 @@ -- @field #AIRBOSS.Checkpoint C3DirtyUp Case III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. -- @field #AIRBOSS.Checkpoint C3BullsEye Case III intercept glideslope and follow ICLS "bullseye". -- @field #number case Recovery case I or III in progress. +-- @field #table flights List of all flights in the CCA. -- @field #table Qmarshal Queue of marshalling aircraft groups. -- @field #table Qpattern Queue of aircraft groups in the landing pattern. -- @field #RESCUEHELO rescuehelo Rescue helo flying in close formation with the carrier. --- @field #CARRIERTANKER tanker Refuelling tanker flying overhead with the carrier. --- @field #table recoverytime Time interval where aircraft are recovered. +-- @field #RECOVERYTANKER tanker Refuelling tanker flying overhead with the carrier. +-- @field #table recoverytime List of time intervals when aircraft are recovered. -- @extends Core.Fsm#FSM --- Practice Carrier Landings -- -- === -- --- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Main.png) +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Main.jpg) -- -- # The AIRBOSS Concept -- @@ -118,6 +120,7 @@ AIRBOSS = { C3DirtyUp = {}, C3BullsEye = {}, case = 1, + flights = {}, Qpattern = {}, Qmarshal = {}, rescuehelo = nil, @@ -144,11 +147,11 @@ AIRBOSS.CarrierType={ STENNIS="Stennis", VINSON="Vinson", TARAWA="LHA_Tarawa", - KUZNETSOV="KUZNECOW" + KUZNETSOV="KUZNECOW", } --- Carrier Parameters. --- @type AIRBOSS.CarrierParameter +-- @type AIRBOSS.CarrierParameters -- @field #number rwyangle Runway angle in degrees. for carriers with angled deck. For USS Stennis -9 degrees. -- @field #number sterndist Distance in meters from carrier position to stern of carrier. For USS Stennis -150 meters. -- @field #number deckheight Height of deck in meters. For USS Stennis ~22 meters. @@ -218,8 +221,8 @@ AIRBOSS.PatternStep={ -- @field #AIRBOSS.RadioSound LONGINGROOVE AIRBOSS.Soundfile={ RIGHTFORLINEUP={ - normal="LSO - RightLineUp(L).ogg", - loud="LSO - RightLineUp(S).ogg", + normal="LSO - RightLineUp(S).ogg", + loud="LSO - RightLineUp(L).ogg", subtitle="Right for line up.", duration=3, }, @@ -364,7 +367,7 @@ AIRBOSS.GroovePos={ -- @field #table Checklist Table of checklist text items to display at this point. --- Marshal and pattern queue items. --- @type AIRBOSS.Queueitem +-- @type AIRBOSS.Flightitem -- @field Wrapper.Group#GROUP group Flight group. -- @field #string groupname Name of the group. -- @field #number nunits Number of units in group. @@ -381,12 +384,13 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.2.7" +AIRBOSS.version="0.2.7w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Add radio check (LSO, AIRBOSS) to F10 radio menu. -- TODO: Monitor holding of players/AI in zoneHolding. -- TODO: Right pattern step after bolter/wo/patternWO? -- TODO: Handle crash event. Delete A/C from queue, send rescue helo, stop carrier? @@ -448,15 +452,13 @@ function AIRBOSS:New(carriername, alias) -- Create carrier beacon. self.beacon=BEACON:New(self.carrier) - -- Set up Airboss radio. - self:SetCarrierradio() + -- Set up Airboss radio. self.Carrierradio=RADIO:New(self.carrier) - self.Carrierradio:SetFrequency(self.Carrierfreq) + self:SetCarrierradio() - -- Set up LSO radio. - self:SetLSOradio() + -- Set up LSO radio. self.LSOradio=RADIO:New(self.carrier) - self.LSOradio:SetFrequency(self.LSOfreq) + self:SetLSOradio() -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then @@ -574,8 +576,6 @@ function AIRBOSS:SetCarrierControlledZone(radius) return self end - - --- Set recovery case pattern. -- @param #AIRBOSS self -- @param #number case Case of recovery. Either 1 or 3. Default 1. @@ -646,6 +646,9 @@ function AIRBOSS:SetLSOradio(frequency, modulation) else self.LSOmodulation=radio.modulation.AM end + + self.LSOradio:SetFrequency(self.LSOfreq) + self.LSOradio:SetModulation(self.LSOmodulation) return self end @@ -665,6 +668,9 @@ function AIRBOSS:SetCarrierradio(frequency, modulation) else self.Carriermodulation=radio.modulation.AM end + + self.Carrierradio:SetFrequency(self.Carrierfreq) + self.Carrierradio:SetModulation(self.Carriermodulation) return self end @@ -672,7 +678,7 @@ end --- Check if carrier is recovering aircraft. -- @param #AIRBOSS self --- @return #boolean If true, time slot for recovery is open. +-- @return #boolean If true, time slot for recovery is open. function AIRBOSS:IsRecovering() return self:is("Recovering") end @@ -1024,7 +1030,7 @@ end -- Queues ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Orbit at a specified position at a specified alititude with a specified speed. +--- Check marshal and pattern queues. -- @param #AIRBOSS self function AIRBOSS:_CheckQueue() @@ -1032,7 +1038,7 @@ function AIRBOSS:_CheckQueue() local nmarshal=#self.Qmarshal for _,_flight in pairs(self.Qpattern) do - local flight=_flight --#AIRBOSS.Queueitem + local flight=_flight --#AIRBOSS.Flightitem npattern=npattern+flight.nunits end @@ -1047,7 +1053,7 @@ function AIRBOSS:_CheckQueue() if nmarshal>0 and npattern<1 then -- First flight send to marshal stack. - local marshalflight=self.Qmarshal[1] --#AIRBOSS.Queueitem + local marshalflight=self.Qmarshal[1] --#AIRBOSS.Flightitem -- Time flight is marshalling. local Tmarshal=timer.getTime()-marshalflight.time @@ -1056,7 +1062,7 @@ function AIRBOSS:_CheckQueue() -- Time (last) flight has entered landing pattern. local Tpattern=999 if npattern>0 then - local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.Queueitem + local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.Flightitem Tpattern=timer.getTime()-patternflight.time env.info(string.format("Pattern time of group %s = %d seconds", patternflight.groupname, Tpattern)) end @@ -1089,7 +1095,7 @@ function AIRBOSS:_PrintQueue(queue, name) text=text.." empty." else for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Queueitem + local flight=_flight --#AIRBOSS.Flightitem local clock=UTILS.SecondsToClock(flight.time) text=text..string.format("\n[%d] %s*%d: stack=%d, flag=%d time=%s", i, flight.groupname, flight.nunits, flight.stack, flight.flag:Get(), clock) end @@ -1111,6 +1117,8 @@ function AIRBOSS:_ScanCarrierZone() -- Scan units in carrier zone. local _,_,_,unitscan=coord:ScanObjects(Rout, true, false, false) + + --[[ -- Inside and outside zones. local zbig=ZONE_RADIUS:New("Bla1", self.carrier:GetVec2(), Rout) @@ -1134,7 +1142,7 @@ function AIRBOSS:_ScanCarrierZone() local groupname=group:GetName() local text=string.format("In carrier zone: unit=%s group=%s", unitname, groupname) - --env.info(text) + --env.info(text) -- Check that it is not already in one of the queues. if not (self:_InQueue(self.Qmarshal, group) or self:_InQueue(self.Qpattern, group)) then @@ -1163,14 +1171,67 @@ function AIRBOSS:_ScanCarrierZone() end end + ]] + + -- Make a table with all groups currently in the zone. + local insideCCA={} + for _,_unit in pairs(unitscan) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Check if this an aircraft and that it is airborn and closing in. + if unit:IsAir() and unit:InAir() and unit:IsInZone(self.zoneCCA)then + + local group=unit:GetGroup() + local groupname=group:GetName() + + if insideCCA[groupname]==nil then + insideCCA[groupname]=group + end + + end + end + + -- Find new flights that are inside CCA. + for groupname,_group in pairs(insideCCA) do + local group=_group --Wrapper.Group#GROUP + + -- Loop over all known flight groups. + local known=false + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.Flightitem + if flight.groupname==groupname then + known=true + break + end + end + + -- Create a new flight group + if not known then + self:_CreateFlightGroup(group) + end + + end + + -- Find flights that are not in CCA. + local remove={} + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.Flightitem + if insideCCA[flight.groupname]==nil then + table.insert(remove, flight.group) + end + end + + -- Remove flight groups. + for _,group in pairs(remove) do + self:_RemoveFlightGroup(group) + end + end ---- Add a flight group. +--- Create a new flight group. Usually when a flight appears in the CCA. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. --- @param #number flagvalue Initial user flag value. --- @param #number alt Altitude in feet. --- @return #AIRBOSS.Queueitem Flight group. +-- @return #AIRBOSS.Flightitem Flight group. function AIRBOSS:_CreateFlightGroup(group) -- Flight group name @@ -1178,22 +1239,45 @@ function AIRBOSS:_CreateFlightGroup(group) local human=self:_IsHuman(group) -- Queue table item. - local qitem={} --#AIRBOSS.Queueitem + local qitem={} --#AIRBOSS.Flightitem qitem.group=group qitem.groupname=group:GetName() qitem.nunits=#group:GetUnits() qitem.fuel=group:GetFuelMin() - qitem.time=timer.getTime() + qitem.time=timer.getAbsTime() qitem.flag=USERFLAG:New(groupname) qitem.flag:Set(-100) qitem.ai=not human if human then - local playerData=self:_GetPlayerDataGroup(group) + + local playerData=self:_GetPlayerDataGroup(group) qitem.player=playerData + + else + + -- Send AI to holding pattern. + self:_MarshalAI(qitem) + end - + return qitem +end + +--- Remove a flight group. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #AIRBOSS.Flightitem Flight group. +function AIRBOSS:_RemoveFlightGroup(group) + local groupname=group:GetName() + for i,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.Flightitem + if flight.groupname==groupname then + self:I(string.format("Removing flight group %s (not in CCA).", groupname)) + table.remove(self.flights, i) + return + end + end end --- Orbit at a specified position at a specified alititude with a specified speed. @@ -1220,20 +1304,21 @@ end --- Tell AI to orbit at a specified position at a specified alititude with a specified speed. -- @param #AIRBOSS self --- @param Wrapper.Group#GROUP group Group -function AIRBOSS:_MarshalAI(group) +-- @param #AIRBOSS.Flightitem flight Flight group. +function AIRBOSS:_MarshalAI(flight) -- Flight group name. - local groupname=group:GetName() + local group=flight.group + local groupname=flight.groupname - -- Number of full marshal stacks. + -- Number of already full marshal stacks. local nstacks=#self.Qmarshal -- Current carrier position. local Carrier=self.carrier:GetCoordinate() -- Aircraft speed when flying the pattern. - local Speed=UTILS.KnotsToMps(250) + local Speed=UTILS.KnotsToMps(272) --- Create a DCS task to orbit at a certain altitude. local function _taskorbit(p1, alt, speed, stopflag, p2) @@ -1252,25 +1337,8 @@ function AIRBOSS:_MarshalAI(group) local n=1 -- Waypoint counter. for stack=nstacks+1,1,-1 do - -- Altitude of first stack. Depends on recovery case. - local angels0 - local Dist - local p1=nil --Core.Point#COORDINATE - local p2=nil --Core.Point#COORDINATE - if self.case==1 then - -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next aircraft. - angels0=2 - Dist=UTILS.NMToMeters(5) - p1=Carrier:Translate(Dist, 270) - else - angels0=6 - Dist=UTILS.NMToMeters((stack-1)*angels0+15) - p1=Carrier:Translate(Dist, self:_Radial()) - p2=Carrier:Translate(Dist+UTILS.NMToMeters(10), self:_Radial()) - end - - -- Pattern altitude. - local Altitude=UTILS.FeetToMeters(((stack-1)+angels0)*1000) + -- Get altitude and positions. + local Altitude, p1, p2=self:_GetMarshalAltitude(stack) -- Orbit task. local TaskOrbit=_taskorbit(p1, Altitude, Speed, stack-1, p2) @@ -1287,19 +1355,9 @@ function AIRBOSS:_MarshalAI(group) -- Landing waypoint. wp[#wp+1]=Carrier:WaypointAirLanding(Speed, self.airbase, nil, "Landing") - - local angels0 - if self.case==1 then - angels0=2 - else - angels0=6 - end - - -- Pattern altitude. - local Altitude=UTILS.FeetToMeters((nstacks+angels0)*1000) - + -- Add group to marshal stack. - self:_AddMarshallGroup(group, nstacks+1, Altitude) + self:_AddMarshallGroup(flight, nstacks+1) -- Reinit waypoints. group:WayPointInitialize(wp) @@ -1308,39 +1366,67 @@ function AIRBOSS:_MarshalAI(group) group:Route(wp, 0) end +--- Get marshal altitude and position. +-- @param #AIRBOSS self +-- @param #number stack Assigned stack number. Counting starts at one, i.e. stack=1 is the first stack. +-- @return #number Holding altitude in meters. +-- @return Core.Point#COORDINATE Holding position coordinate. +-- @return Core.Point#COORDINATE Second holding position coordinate of racetrack pattern for CASE III recoveries. +function AIRBOSS:_GetMarshalAltitude(stack) + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + -- Altitude of first stack. Depends on recovery case. + local angels0 + local Dist + local p1=nil --Core.Point#COORDINATE + local p2=nil --Core.Point#COORDINATE + + if self.case==1 then + -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next stack. + angels0=2 + Dist=UTILS.NMToMeters(5) + p1=Carrier:Translate(Dist, 270) + else + -- CASE III: Holding at 6000 ft on a racetrack pattern astern the carrier. + angels0=6 + Dist=UTILS.NMToMeters((stack-1)*angels0+15) + p1=Carrier:Translate(Dist, self:_Radial()) + p2=Carrier:Translate(Dist+UTILS.NMToMeters(10), self:_Radial()) + end + + -- Pattern altitude. + local altitude=UTILS.FeetToMeters(((stack-1)+angels0)*1000) + + return altitude, p1, p2 +end + --- Add a flight group to the marshal stack. -- @param #AIRBOSS self --- @param Wrapper.Group#GROUP group Aircraft group. --- @param #number flagvalue Initial user flag value. --- @param #number alt Altitude in feet. -function AIRBOSS:_AddMarshallGroup(group, flagvalue, alt) +-- @param #AIRBOSS.Flightitem flight Flight group. +-- @param #number flagvalue Initial user flag value = stack number for holding. +function AIRBOSS:_AddMarshallGroup(flight, flagvalue) - -- Flight group name - local groupname=group:GetName() - - -- Queue table item. - local qitem={} --#AIRBOSS.Queueitem - qitem.group=group - qitem.groupname=group:GetName() - qitem.nunits=#group:GetUnits() - qitem.fuel=group:GetFuelMin() - qitem.time=timer.getTime() - qitem.flag=USERFLAG:New(groupname) - qitem.flag:Set(flagvalue) - qitem.ai=not self:_IsHuman(group) - qitem.stack=alt + -- Set flag value. + flight.flag:Set(flagvalue) -- Pressure. local hPa2inHg=0.0295299830714 local P=self.carrier:GetCoordinate():GetPressure()*hPa2inHg + -- TODO: Get correct board number if possible? + local boardnumber=flight.groupname + local alt=self:_GetMarshalAltitude(flagvalue) + local brc=self:_BaseRecoveryCourse() + -- Marshal message. - local text=string.format("XYZ, Case 1, BRC is 000, hold at %d. Expected Charlie Time XX.\n", qitem.stack) + local text=string.format("%s, Case 1, BRC is %03d, hold at %d. Expected Charlie Time XX.\n", boardnumber, brc, alt) text=text..string.format("Altimeter %.2f. Report see me.", P) MESSAGE:New(text, 30):ToAll() -- Add to marshal queue. - table.insert(self.Qmarshal, qitem) + table.insert(self.Qmarshal, flight) end --- Collapse marshal stack. @@ -1348,7 +1434,7 @@ end function AIRBOSS:_CollapseMarshalStack() for _,_flight in pairs(self.Qmarshal) do - local flight=_flight --#AIRBOSS.Queueitem + local flight=_flight --#AIRBOSS.Flightitem local flagvalue=flight.flag:Get() flight.flag:Set(flagvalue-1) end @@ -1356,11 +1442,11 @@ function AIRBOSS:_CollapseMarshalStack() local nmarshal=#self.Qmarshal for i=nmarshal,1,-1 do - local flight=self.Qmarshal[i] --#AIRBOSS.Queueitem + local flight=self.Qmarshal[i] --#AIRBOSS.Flightitem --flight. end - local flight=self.Qmarshal[1] --#AIRBOSS.Queueitem + local flight=self.Qmarshal[1] --#AIRBOSS.Flightitem env.info(string.format("New pattern flight %s.", flight.groupname)) @@ -1392,7 +1478,7 @@ function AIRBOSS:_RemoveGroupFromQueue(queue, group) local name=group:GetName() for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Queueitem + local flight=_flight --#AIRBOSS.Flightitem if flight.groupname==name then env.info(string.format("FF removing group %s from queue.", name)) @@ -1411,7 +1497,7 @@ function AIRBOSS:_RemoveQueue(queue, group) local name=group:GetName() for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Queueitem + local flight=_flight --#AIRBOSS.Flightitem if flight.groupname==name then @@ -1613,7 +1699,7 @@ function AIRBOSS:OnEventBirth(EventData) 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()) + local text=string.format("AIRBOSS: Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName()) self:T(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) @@ -1634,7 +1720,7 @@ function AIRBOSS:OnEventBirth(EventData) self:_AddF10Commands(_unitName) -- Init player data. - self.players[_playername]=self:_InitPlayer(_unitName) + self.players[_playername]=self:_NewPlayer(_unitName) -- Start in the groove for debugging. self.groovedebug=true @@ -1734,11 +1820,11 @@ end -- PATTERN functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Initialize player data. +--- Initialize player data after birth event of player unit. -- @param #AIRBOSS self -- @param #string unitname Name of the player unit. -- @return #AIRBOSS.PlayerData Player data. -function AIRBOSS:_InitPlayer(unitname) +function AIRBOSS:_NewPlayer(unitname) -- Get player unit and name. local playerunit, playername=self:_GetPlayerUnitAndName(unitname) @@ -1770,7 +1856,7 @@ function AIRBOSS:_InitPlayer(unitname) playerData.inbigzone=playerData.unit:IsInZone(self.zoneCCA) -- Init stuff for this round. - playerData=self:_InitNewApproach(playerData) + playerData=self:_InitPlayer(playerData) -- Return player data table. return playerData @@ -1779,11 +1865,11 @@ function AIRBOSS:_InitPlayer(unitname) return nil end ---- Initialize new approach for player by resetting parmeters to initial values. +--- Initialize player data by (re-)setting parmeters to initial values. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -- @return #AIRBOSS.PlayerData Initialized player data. -function AIRBOSS:_InitNewApproach(playerData) +function AIRBOSS:_InitPlayer(playerData) self:I(self.lid..string.format("New approach of player %s.", playerData.callsign)) playerData.step=AIRBOSS.PatternStep.UNDEFINED @@ -1808,9 +1894,8 @@ function AIRBOSS:_Commencing(playerData) local text="Commencing." - -- Initialize player data for new approach. - self:_InitNewApproach(playerData) + self:_InitPlayer(playerData) -- Next step: depends on case recovery. if self.case==1 then @@ -3546,7 +3631,7 @@ end function AIRBOSS:_InQueue(queue, group) local name=group:GetName() for _,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Queueitem + local flight=_flight --#AIRBOSS.Flightitem if name==flight.groupname then return true end @@ -3987,959 +4072,3 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ---- **Functional** - (R2.5) - Rescue helo. --- --- Recue helicopter on an aircraft carrier --- --- Features: --- --- * Formation with carrier. --- * Automatic respawning on empty fuel. --- --- Please not that his class is work in progress and in an **alpha** stage. --- --- === --- --- ### Author: **funkyfranky** --- --- @module Functional.RescueHelo --- @image MOOSE.JPG - ---- RESCUEHELO class. --- @type RESCUEHELO --- @field #string ClassName Name of the class. --- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. --- @field #string carriertype Carrier type. --- @field #string helogroupname Name of the late activated helo template group. --- @field Wrapper.Group#GROUP helo Helo group. --- @field #number takeoff Takeoff type. --- @field Wrapper.Airbase#AIRBASE airbase The airbase object of the carrier. --- @field Core.Set#SET_GROUP followset Follow group set. --- @field AI.AI_Formation#AI_FORMATION formation AI_FORMATION object. --- @field #number lowfuel Low fuel threshold of helo in percent. --- @extends Core.Fsm#FSM - ---- Rescue Helo --- --- === --- --- ![Banner Image](..\Presentations\RESCUEHELO\RescueHelo_Main.png) --- --- # Recue helo --- --- bla bla --- --- @field #RESCUEHELO -RESCUEHELO = { - ClassName = "RESCUEHELO", - carrier = nil, - carriertype = nil, - helogroupname = nil, - helo = nil, - airbase = nil, - takeoff = nil, - followset = nil, - formation = nil, - lowfuel = nil, -} - ---- Class version. --- @field #string version -RESCUEHELO.version="0.9.0" - --- TODO: Add rescue event. --- TODO: Make offset input parameter. - ---- Constructor. --- @param #RESCUEHELO self --- @param Wrapper.Unit#UNIT carrierunit Carrier unit. --- @param #string helogroupname Name of the late activated rescue helo template group. --- @return #RESCUEHELO RESCUEHELO object. -function RESCUEHELO:New(carrierunit, helogroupname) - - -- Inherit everthing from FSM class. - local self = BASE:Inherit(self, FSM:New()) -- #RESCUEHELO - - if type(carrierunit)=="string" then - self.carrier=UNIT:FindByName(carrierunit) - else - self.carrier=carrierunit - end - - -- Carrier type. - self.carriertype=self.carrier:GetTypeName() - - -- Helo group name. - self.helogroupname=helogroupname - - -- Home airbase of helo - self.airbase=AIRBASE:FindByName(self.carrier:GetName()) - - -- Init defaults. - self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) - self:SetTakeoffHot() - self:SetLowFuelThreshold(10) - - ----------------------- - --- FSM Transitions --- - ----------------------- - - -- Start State. - self:SetStartState("Stopped") - - -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("Stopped", "Start", "Running") - self:AddTransition("Running", "RTB", "Returning") - self:AddTransition("Returning", "Status", "*") - self:AddTransition("Running", "Status", "*") - self:AddTransition("Running", "Stop", "Stopped") - - - --- Triggers the FSM event "Start" that starts the rescue helo. Initializes parameters and starts event handlers. - -- @function [parent=#RESCUEHELO] Start - -- @param #RESCUEHELO self - - --- Triggers the FSM event "Start" that starts the rescue helo after a delay. Initializes parameters and starts event handlers. - -- @function [parent=#RESCUEHELO] __Start - -- @param #RESCUEHELO self - -- @param #number delay Delay in seconds. - - --- Triggers the FSM event "RTB" that sends the helo home. - -- @function [parent=#RESCUEHELO] RTB - -- @param #RESCUEHELO self - - --- Triggers the FSM event "RTB" that sends the helo home after a delay. - -- @function [parent=#RESCUEHELO] __RTB - -- @param #RESCUEHELO self - -- @param #number delay Delay in seconds. - - --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped. - -- @function [parent=#RESCUEHELO] Stop - -- @param #RESCUEHELO self - - --- Triggers the FSM event "Stop" that stops the rescue helo after a delay. Event handlers are stopped. - -- @function [parent=#RESCUEHELO] __Stop - -- @param #RESCUEHELO self - -- @param #number delay Delay in seconds. - - return self - -end - ---- Set low fuel state of helo. When fuel is below this threshold, the helo will RTB or be respawned if takeoff type is in air. --- @param #RESCUEHELO self --- @param #number threshold Low fuel threshold in percent. Default 10. --- @return #RESCUEHELO self -function RESCUEHELO:SetLowFuelThreshold(threshold) - self.lowfuel=threshold or 10 - return self -end - ---- Set home airbase of the helo. Default is the carrier. --- @param #RESCUEHELO self --- @param Wrapper.Airbase#AIRBASE airbase Homebase of helo. --- @return #RESCUEHELO self -function RESCUEHELO:SetHomeBase(airbase) - self.airbase=airbase - return self -end - ---- Set takeoff type. --- @param #RESCUEHELO self --- @param #number takeofftype Takeoff type. --- @return #RESCUEHELO self -function RESCUEHELO:SetTakeoff(takeofftype) - self.takeoff=takeofftype - return self -end - ---- Set takeoff with engines running (hot). --- @param #RESCUEHELO self --- @return #RESCUEHELO self -function RESCUEHELO:SetTakeoffHot() - self:SetTakeoff(SPAWN.Takeoff.Hot) - return self -end - ---- Set takeoff with engines off (cold). --- @param #RESCUEHELO self --- @return #RESCUEHELO self -function RESCUEHELO:SetTakeoffCold() - self:SetTakeoff(SPAWN.Takeoff.Cold) - return self -end - ---- Set takeoff in air near the carrier. --- @param #RESCUEHELO self --- @return #RESCUEHELO self -function RESCUEHELO:SetTakeoffAir() - self:SetTakeoff(SPAWN.Takeoff.Air) - return self -end - - ---- Check if tanker is returning to base. --- @param #RESCUEHELO self --- @return #boolean If true, helo is returning to base. -function RESCUEHELO:IsReturning() - return self:is("Returning") -end - ---- Check if tanker is operating. --- @param #RESCUEHELO self --- @return #boolean If true, helo is operating. -function RESCUEHELO:IsRunning() - return self:is("Running") -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- FSM states -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. --- @param #RESCUEHELO self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function RESCUEHELO:onafterStart(From, Event, To) - - -- Events are handled my MOOSE. - self:I(string.format("Starting Rescue Helo Formation v%s for carrier unit %s of type %s.", RESCUEHELO.version, self.carrier:GetName(), self.carriertype)) - - -- Handle events. - --self:HandleEvent(EVENTS.Birth) - self:HandleEvent(EVENTS.Land) - --self:HandleEvent(EVENTS.Crash) - - -- Offset [meters] in the direction of travelling. Positive values are in front of Mother. - local OffsetX=200 - -- Offset [meters] perpendicular to travelling. Positive = Starboard (right of Mother), negative = Port (left of Mother). - local OffsetZ=200 - -- Offset altitude. Should (obviously) always be positve. - local OffsetY=70 - - -- Delay before formation is started. - local delay=120 - - -- Spawn helo. - local Spawn=SPAWN:New(self.helogroupname):InitUnControlled(false) - - -- Spawn in air or at airbase. - if self.takeoff==SPAWN.Takeoff.Air then - - -- Carrier heading - local hdg=self.carrier:GetHeading() - - -- Spawn distance behind carrier. - local dist=UTILS.NMToMeters(0.2) - - -- Coordinate behind the carrier - local Carrier=self.carrier:GetCoordinate():SetAltitude(OffsetY):Translate(dist, hdg) - - -- Orientation of spawned group. - Spawn:InitHeading(hdg) - - -- Spawn at coordinate. - self.helo=Spawn:SpawnFromCoordinate(Carrier) - - -- Start formation in 1 seconds - delay=1 - - else - - -- Spawn at airbase. - self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) - - if self.takeoff==SPAWN.Takeoff.Runway then - delay=5 - elseif self.takeoff==SPAWN.Takeoff.Hot then - delay=30 - elseif self.takeoff==SPAWN.Takeoff.Cold then - delay=60 - end - - end - - -- Set of group(s) to follow Mother. - self.followset=SET_GROUP:New() - self.followset:AddGroup(self.helo) - - -- Get initial fuel. - self.HeloFuel0=self.helo:GetFuel() - - -- Define AI Formation object. - self.formation=AI_FORMATION:New(self.carrier, self.followset, "Helo Formation with Carrier", "Follow Carrier at given parameters.") - - -- Formation parameters. - self.formation:FormationCenterWing(-OffsetX, 50, math.abs(OffsetY), 50, OffsetZ, 50) - - -- Start formation FSM. - self.formation:__Start(delay) - - -- Start uncontrolled helo. - --HeloSpawn:StartUncontrolled(120) - - -- Init status check - self:__Status(1) - -end - ---- On after Status event. Checks player status. --- @param #RESCUEHELO self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function RESCUEHELO:onafterStatus(From, Event, To) - - -- Get current time. - local time=timer.getTime() - - -- Get relative fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712) - local fuel=self.helo:GetFuel()/self.HeloFuel0*100 - - -- Report current fuel. - local text=string.format("Rescue Helo %s: state=%s fuel=%.1f", self.helo:GetName(), self:GetState(), fuel) - self:I(text) - - -- If fuel < threshold ==> send helo to home base! - if fuel Event --> To State - self:AddTransition("Stopped", "Start", "Running") - self:AddTransition("Running", "RTB", "Returning") - self:AddTransition("Running", "Status", "*") - self:AddTransition("Returning", "Status", "*") - self:AddTransition("Running", "Stop", "Stopped") - - - --- Triggers the FSM event "Start" that starts the carrier tanker. Initializes parameters and starts event handlers. - -- @function [parent=#CARRIERTANKER] Start - -- @param #CARRIERTANKER self - - --- Triggers the FSM event "Start" that starts the carrier tanker after a delay. Initializes parameters and starts event handlers. - -- @function [parent=#CARRIERTANKER] __Start - -- @param #CARRIERTANKER self - -- @param #number delay Delay in seconds. - - --- Triggers the FSM event "RTB" that sends the tanker home. - -- @function [parent=#CARRIERTANKER] RTB - -- @param #CARRIERTANKER self - - --- Triggers the FSM event "RTB" that sends the tanker home after a delay. - -- @function [parent=#CARRIERTANKER] __RTB - -- @param #CARRIERTANKER self - -- @param #number delay Delay in seconds. - - --- Triggers the FSM event "Stop" that stops the carrier tanker. Event handlers are stopped. - -- @function [parent=#CARRIERTANKER] Stop - -- @param #CARRIERTANKER self - - --- Triggers the FSM event "Stop" that stops the carrier tanker after a delay. Event handlers are stopped. - -- @function [parent=#CARRIERTANKER] __Stop - -- @param #CARRIERTANKER self - -- @param #number delay Delay in seconds. - - return self -end - ---- Set the speed the tanker flys in its orbit pattern. --- @param #CARRIERTANKER self --- @param #number speed Tanker speed in knots. --- @return #CARRIERTANKER self -function CARRIERTANKER:SetSpeed(speed) - self.speed=UTILS.KnotsToMps(speed) - return self -end - ---- Set orbit pattern altitude of the tanker. --- @param #CARRIERTANKER self --- @param #number altitude Tanker altitude in feet. --- @return #CARRIERTANKER self -function CARRIERTANKER:SetAltitude(altitude) - self.altitude=UTILS.FeetToMeters(altitude) - return self -end - ---- Set race-track distances. --- @param #CARRIERTANKER self --- @param #number distbow Distance [NM] in front of the carrier. Default 6 NM. --- @param #number diststern Distance [NM] behind the carrier. Default 8 NM. --- @return #CARRIERTANKER self -function CARRIERTANKER:SetRacetrackDistances(distbow, diststern) - self.distBow=UTILS.NMToMeters(distbow or 6) - self.distStern=-UTILS.NMToMeters(diststern or 8) - return self -end - ---- Set pattern update interval. Note that this update causes a slight disruption in the race track pattern. --- Therefore, the interval should be as long as possible but short enough to keep the tanker overhead the carrier. --- @param #CARRIERTANKER self --- @param #number interval Interval in minutes. Default is every 30 minutes. --- @return #CARRIERTANKER self -function CARRIERTANKER:SetPatternUpdateInterval(interval) - self.dTupdate=(interval or 30)*60 - return self -end - ---- Set low fuel state of tanker. When fuel is below this threshold, the tanker will RTB or be respawned if takeoff type is in air. --- @param #CARRIERTANKER self --- @param #number threshold Low fuel threshold in percent. Default 10. --- @return #CARRIERTANKER self -function CARRIERTANKER:SetLowFuelThreshold(threshold) - self.lowfuel=threshold or 10 - return self -end - ---- Set home airbase of the tanker. Default is the carrier. --- @param #CARRIERTANKER self --- @param Wrapper.Airbase#AIRBASE airbase --- @return #CARRIERTANKER self -function CARRIERTANKER:SetHomeBase(airbase) - self.airbase=airbase - return self -end - ---- Set takeoff type. --- @param #CARRIERTANKER self --- @param #number takeofftype Takeoff type. --- @return #CARRIERTANKER self -function CARRIERTANKER:SetTakeoff(takeofftype) - self.takeoff=takeofftype - return self -end - ---- Set takeoff with engines running (hot). --- @param #CARRIERTANKER self --- @return #CARRIERTANKER self -function CARRIERTANKER:SetTakeoffHot() - self:SetTakeoff(SPAWN.Takeoff.Hot) - return self -end - ---- Set takeoff with engines off (cold). --- @param #CARRIERTANKER self --- @return #CARRIERTANKER self -function CARRIERTANKER:SetTakeoffCold() - self:SetTakeoff(SPAWN.Takeoff.Cold) - return self -end - ---- Set takeoff in air at pattern altitude 30 NM behind the carrier. --- @param #CARRIERTANKER self --- @return #CARRIERTANKER self -function CARRIERTANKER:SetTakeoffAir() - self:SetTakeoff(SPAWN.Takeoff.Air) - return self -end - - ---- Check if tanker is returning to base. --- @param #CARRIERTANKER self --- @return #boolean If true, tanker is returning to base. -function CARRIERTANKER:IsReturning() - return self:is("Returning") -end - ---- Check if tanker is operating. --- @param #CARRIERTANKER self --- @return #boolean If true, tanker is operating. -function CARRIERTANKER:IsRunning() - return self:is("Running") -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- FSM states -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. --- @param #CARRIERTANKER self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function CARRIERTANKER:onafterStart(From, Event, To) - - -- Info on start. - self:I(string.format("Starting Carrier Tanker v%s for carrier unit %s of type %s for tanker group %s.", CARRIERTANKER.version, self.carrier:GetName(), self.carriertype, self.tankergroupname)) - - -- Handle events. - self:HandleEvent(EVENTS.EngineShutdown) - --TODO: Handle event crash and respawn. - - -- Spawn tanker. - local Spawn=SPAWN:New(self.tankergroupname):InitUnControlled(false) - - -- Spawn on carrier. - if self.takeoff==SPAWN.Takeoff.Air then - - -- Carrier heading - local hdg=self.carrier:GetHeading() - - local dist=UTILS.NMToMeters(20) - - -- Coordinate behind the carrier - local Carrier=self.carrier:GetCoordinate():SetAltitude(self.altitude):Translate(-dist, hdg) - - -- Orientation of spawned group. - Spawn:InitHeading(hdg) - - -- Spawn at coordinate. - self.tanker=Spawn:SpawnFromCoordinate(Carrier) - - self:_InitRoute(15, 1, 2) - else - - -- Spawn tanker at airbase. - self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) - self:_InitRoute(30, 10, 1) - - end - - -- Init status check. - self:__Status(10) -end - ---- On after Status event. Checks player status. --- @param #CARRIERTANKER self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function CARRIERTANKER:onafterStatus(From, Event, To) - - -- Get current time. - local time=timer.getTime() - - -- Get fuel of tanker. - local fuel=self.tanker:GetFuel()*100 - local text=string.format("Tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) - self:I(text) - - - if self:IsRunning() then - - -- Check fuel. - if fuelself.dTupdate then - self:_PatternUpdate() - end - - end - end - - end - - -- Call status again in 1 minute. - self:__Status(-60) -end - ---- On after Stop event. Unhandle events and stop status updates. --- @param #CARRIERTANKER self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function CARRIERTANKER:onafterStop(From, Event, To) - self:UnHandleEvent(EVENTS.EngineShutdown) - --self:UnHandleEvent(EVENTS.Land) -end - ---- On before RTB event. Check if takeoff type is air and if so respawn the tanker and deny RTB transition. --- @param #CARRIERTANKER self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @return #boolean If true, transition is allowed. -function CARRIERTANKER:onbeforeRTB(From, Event, To) - - if self.takeoff==SPAWN.Takeoff.Air then - - -- Debug message. - local text=string.format("Respawning tanker %s.", self.tanker:GetName()) - self:I(text) - - -- Respawn tanker. - self.tanker:InitHeading(self.tanker:GetHeading()) - self.tanker=self.tanker:Respawn(nil, true) - - -- Update Pattern in 2 seconds. Need to give a bit time so that the respawned group is in the game. - SCHEDULER:New(nil, self._PatternUpdate, {self}, 2) - - -- Deny transition to RTB. - return false - end - - return true -end - ---- On after RTB event. Send tanker back to carrier. --- @param #CARRIERTANKER self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function CARRIERTANKER:onafterRTB(From, Event, To) - - -- Debug message. - local text=string.format("Tanker %s returning to airbase %s.", self.tanker:GetName(), self.airbase:GetName()) - self:I(text) - - local waypoints={} - - -- Set landingwaypoint - local wp=self.carrier:GetCoordinate():WaypointAirLanding(300, self.airbase, nil, "Landing") - table.insert(waypoints, wp) - - -- Initialize WP and route tanker. - self.tanker:WayPointInitialize(waypoints) - - -- Set task. - self.tanker:Route(waypoints, 1) -end - - ---- Event handler for engine shutdown of carrier tanker. --- Respawn tanker group once it landed because it was out of fuel. --- @param #CARRIERTANKER self --- @param Core.Event#EVENTDATA EventData Event data. -function CARRIERTANKER:OnEventEngineShutdown(EventData) - - local group=EventData.IniGroup --Wrapper.Group#GROUP - - if group:IsAlive() then - - -- Group name. When spawning it will have #001 attached. - local groupname=group:GetName() - - if groupname:match(self.tankergroupname) then - - -- Debug info. - self:I(string.format("CARIERTANKER: Respawning group %s.", group:GetName())) - - -- Respawn tanker. - self.tanker=group:RespawnAtCurrentAirbase() - - --group:StartUncontrolled(60) - - -- Initial route. - self:_InitRoute() - end - - end -end - - ---- Init waypoint after spawn. --- @param #CARRIERTANKER self --- @param #number dist Distance [NM] of initial waypoint astern carrier. Default 30 NM. --- @param #number Tstart Time in minutes before the tanker starts its pattern. Default 10 min. --- @param #number delay Delay before routing in seconds. Default 1 second. -function CARRIERTANKER:_InitRoute(dist, Tstart, delay) - - -- Defaults. - dist=UTILS.NMToMeters(dist or 30) - Tstart=(Tstart or 10)*60 - delay=delay or 1 - - -- Debug message. - self:I(string.format("Initializing route for tanker %s.", self.tanker:GetName())) - - -- Carrier position. - local Carrier=self.carrier:GetCoordinate() - - -- Carrier heading. - local hdg=self.carrier:GetHeading() - - -- First waypoint is 50 km behind the boat. - local p=Carrier:Translate(-dist, hdg):SetAltitude(self.altitude) - - -- Debug mark - p:MarkToAll(string.format("Init WP: alt=%d ft, speed=%d kts", UTILS.MetersToFeet(self.altitude), UTILS.MpsToKnots(self.speed))) - - -- Waypoints. - local wp={} - wp[1]=Carrier:WaypointAirTakeOffParking() - wp[2]=p:WaypointAirTurningPoint(nil, self.speed, nil, "Stern") - - -- Set route. - self.tanker:Route(wp, delay) - - -- No update yet. - self.Tupdate=nil - - -- Update pattern in ~10 minutes. - SCHEDULER:New(nil, self._PatternUpdate, {self}, Tstart) -end - - ---- Function to update the race-track pattern of the tanker wrt to the carrier position. --- @param #CARRIERTANKER self -function CARRIERTANKER:_PatternUpdate() - - -- Carrier heading. - local hdg=self.carrier:GetHeading() - - -- Carrier position. - local Carrier=self.carrier:GetCoordinate() - - -- Define race-track pattern. - local p1=Carrier:SetAltitude(self.altitude):Translate(self.distStern, hdg) - local p2=Carrier:SetAltitude(self.altitude):Translate(self.distBow, hdg) - - -- Set orbit task. - local taskorbit=self.tanker:TaskOrbit(p1, self.altitude, self.speed, p2) - - -- New waypoint. - local p0=self.tanker:GetCoordinate():Translate(1000, self.tanker:GetHeading()) - - -- Debug markers. - if self.Debug then - p0:MarkToAll("p0") - p1:MarkToAll("p1") - p2:MarkToAll("p2") - end - - -- Debug message. - self:I(string.format("Updating tanker %s orbit.", self.tanker:GetName())) - - -- Waypoints array. - local waypoints={} - - -- New waypoint with orbit pattern task. - local wp=p0:WaypointAirTurningPoint(nil, self.speed, {taskorbit}, "Tanker Orbit") - waypoints[1]=wp - - -- Initialize WP and route tanker. - self.tanker:WayPointInitialize(waypoints) - - -- Task combo. - local tasktanker = self.tanker:EnRouteTaskTanker() - local taskroute = self.tanker:TaskRoute(waypoints) - local taskcombo = self.tanker:TaskCombo({tasktanker, taskroute}) - - -- Set task. - self.tanker:SetTask(taskcombo, 1) - - -- Set update time. - self.Tupdate=timer.getTime() -end diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua new file mode 100644 index 000000000..1af9412e2 --- /dev/null +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -0,0 +1,558 @@ +--- **Functional** - (R2.5) - Carrier recovery tanker. +-- +-- Tanker aircraft flying a racetrack pattern overhead an aircraft carrier. +-- +-- Features: +-- +-- * Regular pattern update with respect to carrier positon. +-- * Automatic respawning when tanker runs out of fuel. +-- * Tanker can be spawned cold or hot on the carrier or any other airbase or directly in air. +-- * Tanker can operate 24/7. +-- +-- Please not that his class is work in progress and in an **alpha** stage. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- @module Ops.CarrierTanker +-- @image MOOSE.JPG + +--- RECOVERYTANKER class. +-- @type RECOVERYTANKER +-- @field #string ClassName Name of the class. +-- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. +-- @field #string carriertype Carrier type. +-- @field #string tankergroupname Name of the late activated tanker template group. +-- @field Wrapper.Group#GROUP tanker Tanker group. +-- @field Wrapper.Airbase#AIRBASE airbase The home airbase object of the tanker. Normally the aircraft carrier. +-- @field #number speed Tanker speed when flying pattern. +-- @field #number altitude Tanker orbit pattern altitude. +-- @field #number distStern Race-track distance astern. +-- @field #number distBow Race-track distance bow. +-- @field #number dTupdate Time interval for updating pattern position wrt new tanker position. +-- @field #number Tupdate Last time the pattern was updated. +-- @field #number takeoff Takeoff type (cold, hot, air). +-- @field #number lowfuel Low fuel threshold in percent. +-- @extends Core.Fsm#FSM + +--- Carrier Tanker. +-- +-- === +-- +-- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Main.jpg) +-- +-- # Carrier Tanker +-- +-- bla bla +-- +-- @field #RECOVERYTANKER +RECOVERYTANKER = { + ClassName = "RECOVERYTANKER", + carrier = nil, + carriertype = nil, + tankergroupname = nil, + tanker = nil, + airbase = nil, + altitude = nil, + speed = nil, + distStern = nil, + distBow = nil, + dTupdate = nil, + Tupdate = nil, + takeoff = nil, + lowfuel = nil, +} + + +--- Class version. +-- @field #string version +RECOVERYTANKER.version="0.9.0w" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Write documenation. +-- TODO: Smarter pattern update function. E.g. (small) zone around carrier. Only update position when carrier leaves zone or changes heading? +-- TODO: Maybe rework pattern update implementation altogether to make it smoother. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create new RECOVERYTANKER object. +-- @param #RECOVERYTANKER self +-- @param Wrapper.Unit#UNIT carrierunit Carrier unit. +-- @param #string tankergroupname Name of the late activated tanker aircraft template group. +-- @return #RECOVERYTANKER RECOVERYTANKER object. +function RECOVERYTANKER:New(carrierunit, tankergroupname) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #RECOVERYTANKER + + if type(carrierunit)=="string" then + self.carrier=UNIT:FindByName(carrierunit) + else + self.carrier=carrierunit + end + + -- Carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Tanker group name. + self.tankergroupname=tankergroupname + + -- Default parameters. + self:SetPatternUpdateInterval() + self:SetAltitude() + self:SetSpeed() + self:SetRacetrackDistances(6, 8) + self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) + self:SetTakeoffAir() + self:SetLowFuelThreshold() + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("Running", "RTB", "Returning") + self:AddTransition("Running", "Status", "*") + self:AddTransition("Returning", "Status", "*") + self:AddTransition("Running", "Stop", "Stopped") + + + --- Triggers the FSM event "Start" that starts the carrier tanker. Initializes parameters and starts event handlers. + -- @function [parent=#RECOVERYTANKER] Start + -- @param #RECOVERYTANKER self + + --- Triggers the FSM event "Start" that starts the carrier tanker after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#RECOVERYTANKER] __Start + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "RTB" that sends the tanker home. + -- @function [parent=#RECOVERYTANKER] RTB + -- @param #RECOVERYTANKER self + + --- Triggers the FSM event "RTB" that sends the tanker home after a delay. + -- @function [parent=#RECOVERYTANKER] __RTB + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop" that stops the carrier tanker. Event handlers are stopped. + -- @function [parent=#RECOVERYTANKER] Stop + -- @param #RECOVERYTANKER self + + --- Triggers the FSM event "Stop" that stops the carrier tanker after a delay. Event handlers are stopped. + -- @function [parent=#RECOVERYTANKER] __Stop + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set the speed the tanker flys in its orbit pattern. +-- @param #RECOVERYTANKER self +-- @param #number speed Tanker speed in knots. Default 272 knots. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetSpeed(speed) + self.speed=UTILS.KnotsToMps(speed or 272) + return self +end + +--- Set orbit pattern altitude of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number altitude Tanker altitude in feet. Default 6000 ft. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetAltitude(altitude) + self.altitude=UTILS.FeetToMeters(altitude or 6000) + return self +end + +--- Set race-track distances. +-- @param #RECOVERYTANKER self +-- @param #number distbow Distance [NM] in front of the carrier. Default 6 NM. +-- @param #number diststern Distance [NM] behind the carrier. Default 8 NM. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRacetrackDistances(distbow, diststern) + self.distBow=UTILS.NMToMeters(distbow or 6) + self.distStern=-UTILS.NMToMeters(diststern or 8) + return self +end + +--- Set pattern update interval. Note that this update causes a slight disruption in the race track pattern. +-- Therefore, the interval should be as long as possible but short enough to keep the tanker overhead the carrier. +-- @param #RECOVERYTANKER self +-- @param #number interval Interval in minutes. Default is every 30 minutes. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateInterval(interval) + self.dTupdate=(interval or 30)*60 + return self +end + +--- Set low fuel state of tanker. When fuel is below this threshold, the tanker will RTB or be respawned if takeoff type is in air. +-- @param #RECOVERYTANKER self +-- @param #number threshold Low fuel threshold in percent. Default 10. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetLowFuelThreshold(threshold) + self.lowfuel=threshold or 10 + return self +end + +--- Set home airbase of the tanker. Default is the carrier. +-- @param #RECOVERYTANKER self +-- @param Wrapper.Airbase#AIRBASE airbase +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetHomeBase(airbase) + self.airbase=airbase + return self +end + +--- Set takeoff type. +-- @param #RECOVERYTANKER self +-- @param #number takeofftype Takeoff type. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoff(takeofftype) + self.takeoff=takeofftype + return self +end + +--- Set takeoff with engines running (hot). +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffHot() + self:SetTakeoff(SPAWN.Takeoff.Hot) + return self +end + +--- Set takeoff with engines off (cold). +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffCold() + self:SetTakeoff(SPAWN.Takeoff.Cold) + return self +end + +--- Set takeoff in air at pattern altitude 30 NM behind the carrier. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffAir() + self:SetTakeoff(SPAWN.Takeoff.Air) + return self +end + + +--- Check if tanker is returning to base. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is returning to base. +function RECOVERYTANKER:IsReturning() + return self:is("Returning") +end + +--- Check if tanker is operating. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is operating. +function RECOVERYTANKER:IsRunning() + return self:is("Running") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RECOVERYTANKER:onafterStart(From, Event, To) + + -- Info on start. + self:I(string.format("Starting Carrier Tanker v%s for carrier unit %s of type %s for tanker group %s.", RECOVERYTANKER.version, self.carrier:GetName(), self.carriertype, self.tankergroupname)) + + -- Handle events. + self:HandleEvent(EVENTS.EngineShutdown) + --TODO: Handle event crash and respawn. + + -- Spawn tanker. + local Spawn=SPAWN:New(self.tankergroupname):InitUnControlled(false) + + -- Spawn on carrier. + if self.takeoff==SPAWN.Takeoff.Air then + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + local dist=UTILS.NMToMeters(20) + + -- Coordinate behind the carrier + local Carrier=self.carrier:GetCoordinate():SetAltitude(self.altitude):Translate(-dist, hdg) + + -- Orientation of spawned group. + Spawn:InitHeading(hdg) + + -- Spawn at coordinate. + self.tanker=Spawn:SpawnFromCoordinate(Carrier) + + self:_InitRoute(15, 1, 2) + else + + -- Spawn tanker at airbase. + self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) + self:_InitRoute(30, 10, 1) + + end + + -- Init status check. + self:__Status(10) +end + +--- On after Status event. Checks player status. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RECOVERYTANKER:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Get fuel of tanker. + local fuel=self.tanker:GetFuel()*100 + local text=string.format("Tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) + self:I(text) + + + if self:IsRunning() then + + -- Check fuel. + if fuelself.dTupdate then + self:_PatternUpdate() + end + + end + end + + end + + -- Call status again in 1 minute. + self:__Status(-60) +end + +--- On after Stop event. Unhandle events and stop status updates. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RECOVERYTANKER:onafterStop(From, Event, To) + self:UnHandleEvent(EVENTS.EngineShutdown) + --self:UnHandleEvent(EVENTS.Land) +end + +--- On before RTB event. Check if takeoff type is air and if so respawn the tanker and deny RTB transition. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean If true, transition is allowed. +function RECOVERYTANKER:onbeforeRTB(From, Event, To) + + if self.takeoff==SPAWN.Takeoff.Air then + + -- Debug message. + local text=string.format("Respawning tanker %s.", self.tanker:GetName()) + self:I(text) + + -- Respawn tanker. + self.tanker:InitHeading(self.tanker:GetHeading()) + self.tanker=self.tanker:Respawn(nil, true) + + -- Update Pattern in 2 seconds. Need to give a bit time so that the respawned group is in the game. + SCHEDULER:New(nil, self._PatternUpdate, {self}, 2) + + -- Deny transition to RTB. + return false + end + + return true +end + +--- On after RTB event. Send tanker back to carrier. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RECOVERYTANKER:onafterRTB(From, Event, To) + + -- Debug message. + local text=string.format("Tanker %s returning to airbase %s.", self.tanker:GetName(), self.airbase:GetName()) + self:I(text) + + local waypoints={} + + -- Set landingwaypoint + local wp=self.carrier:GetCoordinate():WaypointAirLanding(300, self.airbase, nil, "Landing") + table.insert(waypoints, wp) + + -- Initialize WP and route tanker. + self.tanker:WayPointInitialize(waypoints) + + -- Set task. + self.tanker:Route(waypoints, 1) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event handler for engine shutdown of carrier tanker. +-- Respawn tanker group once it landed because it was out of fuel. +-- @param #RECOVERYTANKER self +-- @param Core.Event#EVENTDATA EventData Event data. +function RECOVERYTANKER:OnEventEngineShutdown(EventData) + + local group=EventData.IniGroup --Wrapper.Group#GROUP + + if group:IsAlive() then + + -- Group name. When spawning it will have #001 attached. + local groupname=group:GetName() + + if groupname:match(self.tankergroupname) then + + -- Debug info. + self:I(string.format("CARIERTANKER: Respawning group %s.", group:GetName())) + + -- Respawn tanker. + self.tanker=group:RespawnAtCurrentAirbase() + + --group:StartUncontrolled(60) + + -- Initial route. + self:_InitRoute() + end + + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ROUTE functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Init waypoint after spawn. +-- @param #RECOVERYTANKER self +-- @param #number dist Distance [NM] of initial waypoint astern carrier. Default 30 NM. +-- @param #number Tstart Time in minutes before the tanker starts its pattern. Default 10 min. +-- @param #number delay Delay before routing in seconds. Default 1 second. +function RECOVERYTANKER:_InitRoute(dist, Tstart, delay) + + -- Defaults. + dist=UTILS.NMToMeters(dist or 30) + Tstart=(Tstart or 10)*60 + delay=delay or 1 + + -- Debug message. + self:I(string.format("Initializing route for tanker %s.", self.tanker:GetName())) + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- First waypoint is 50 km behind the boat. + local p=Carrier:Translate(-dist, hdg):SetAltitude(self.altitude) + + -- Debug mark + p:MarkToAll(string.format("Init WP: alt=%d ft, speed=%d kts", UTILS.MetersToFeet(self.altitude), UTILS.MpsToKnots(self.speed))) + + -- Waypoints. + local wp={} + wp[1]=Carrier:WaypointAirTakeOffParking() + wp[2]=p:WaypointAirTurningPoint(nil, self.speed, nil, "Stern") + + -- Set route. + self.tanker:Route(wp, delay) + + -- No update yet. + self.Tupdate=nil + + -- Update pattern in ~10 minutes. + SCHEDULER:New(nil, self._PatternUpdate, {self}, Tstart) +end + + +--- Function to update the race-track pattern of the tanker wrt to the carrier position. +-- @param #RECOVERYTANKER self +function RECOVERYTANKER:_PatternUpdate() + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + -- Define race-track pattern. + local p1=Carrier:SetAltitude(self.altitude):Translate(self.distStern, hdg) + local p2=Carrier:SetAltitude(self.altitude):Translate(self.distBow, hdg) + + -- Set orbit task. + local taskorbit=self.tanker:TaskOrbit(p1, self.altitude, self.speed, p2) + + -- New waypoint. + local p0=self.tanker:GetCoordinate():Translate(1000, self.tanker:GetHeading()) + + -- Debug markers. + if self.Debug then + p0:MarkToAll("p0") + p1:MarkToAll("p1") + p2:MarkToAll("p2") + end + + -- Debug message. + self:I(string.format("Updating tanker %s orbit.", self.tanker:GetName())) + + -- Waypoints array. + local waypoints={} + + -- New waypoint with orbit pattern task. + local wp=p0:WaypointAirTurningPoint(nil, self.speed, {taskorbit}, "Tanker Orbit") + waypoints[1]=wp + + -- Initialize WP and route tanker. + self.tanker:WayPointInitialize(waypoints) + + -- Task combo. + local tasktanker = self.tanker:EnRouteTaskTanker() + local taskroute = self.tanker:TaskRoute(waypoints) + local taskcombo = self.tanker:TaskCombo({tasktanker, taskroute}) + + -- Set task. + self.tanker:SetTask(taskcombo, 1) + + -- Set update time. + self.Tupdate=timer.getTime() +end diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua new file mode 100644 index 000000000..0c638e004 --- /dev/null +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -0,0 +1,432 @@ +--- **Functional** - (R2.5) - Rescue helo. +-- +-- Recue helicopter on an aircraft carrier. +-- +-- Features: +-- +-- * Formation with carrier. +-- * Automatic respawning on empty fuel. +-- +-- Please not that his class is work in progress and in an **alpha** stage. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- @module Ops.RescueHelo +-- @image MOOSE.JPG + +--- RESCUEHELO class. +-- @type RESCUEHELO +-- @field #string ClassName Name of the class. +-- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. +-- @field #string carriertype Carrier type. +-- @field #string helogroupname Name of the late activated helo template group. +-- @field Wrapper.Group#GROUP helo Helo group. +-- @field #number takeoff Takeoff type. +-- @field Wrapper.Airbase#AIRBASE airbase The airbase object of the carrier. +-- @field Core.Set#SET_GROUP followset Follow group set. +-- @field AI.AI_Formation#AI_FORMATION formation AI_FORMATION object. +-- @field #number lowfuel Low fuel threshold of helo in percent. +-- @extends Core.Fsm#FSM + +--- Rescue Helo +-- +-- === +-- +-- ![Banner Image](..\Presentations\RESCUEHELO\RescueHelo_Main.jpg) +-- +-- # Recue helo +-- +-- bla bla +-- +-- @field #RESCUEHELO +RESCUEHELO = { + ClassName = "RESCUEHELO", + carrier = nil, + carriertype = nil, + helogroupname = nil, + helo = nil, + airbase = nil, + takeoff = nil, + followset = nil, + formation = nil, + lowfuel = nil, +} + +--- Class version. +-- @field #string version +RESCUEHELO.version="0.9.0w" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Write documenation. +-- TODO: Add rescue event when aircraft crashes. +-- TODO: Make offset input parameter. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new RESCUEHELO object. +-- @param #RESCUEHELO self +-- @param Wrapper.Unit#UNIT carrierunit Carrier unit. +-- @param #string helogroupname Name of the late activated rescue helo template group. +-- @return #RESCUEHELO RESCUEHELO object. +function RESCUEHELO:New(carrierunit, helogroupname) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #RESCUEHELO + + if type(carrierunit)=="string" then + self.carrier=UNIT:FindByName(carrierunit) + else + self.carrier=carrierunit + end + + -- Carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Helo group name. + self.helogroupname=helogroupname + + -- Home airbase of helo + self.airbase=AIRBASE:FindByName(self.carrier:GetName()) + + -- Init defaults. + self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) + self:SetTakeoffHot() + self:SetLowFuelThreshold(10) + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("Running", "RTB", "Returning") + self:AddTransition("Returning", "Status", "*") + self:AddTransition("Running", "Status", "*") + self:AddTransition("Running", "Stop", "Stopped") + + + --- Triggers the FSM event "Start" that starts the rescue helo. Initializes parameters and starts event handlers. + -- @function [parent=#RESCUEHELO] Start + -- @param #RESCUEHELO self + + --- Triggers the FSM event "Start" that starts the rescue helo after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#RESCUEHELO] __Start + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "RTB" that sends the helo home. + -- @function [parent=#RESCUEHELO] RTB + -- @param #RESCUEHELO self + + --- Triggers the FSM event "RTB" that sends the helo home after a delay. + -- @function [parent=#RESCUEHELO] __RTB + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped. + -- @function [parent=#RESCUEHELO] Stop + -- @param #RESCUEHELO self + + --- Triggers the FSM event "Stop" that stops the rescue helo after a delay. Event handlers are stopped. + -- @function [parent=#RESCUEHELO] __Stop + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set low fuel state of helo. When fuel is below this threshold, the helo will RTB or be respawned if takeoff type is in air. +-- @param #RESCUEHELO self +-- @param #number threshold Low fuel threshold in percent. Default 10. +-- @return #RESCUEHELO self +function RESCUEHELO:SetLowFuelThreshold(threshold) + self.lowfuel=threshold or 10 + return self +end + +--- Set home airbase of the helo. Default is the carrier. +-- @param #RESCUEHELO self +-- @param Wrapper.Airbase#AIRBASE airbase Homebase of helo. +-- @return #RESCUEHELO self +function RESCUEHELO:SetHomeBase(airbase) + self.airbase=airbase + return self +end + +--- Set takeoff type. +-- @param #RESCUEHELO self +-- @param #number takeofftype Takeoff type. +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoff(takeofftype) + self.takeoff=takeofftype + return self +end + +--- Set takeoff with engines running (hot). +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffHot() + self:SetTakeoff(SPAWN.Takeoff.Hot) + return self +end + +--- Set takeoff with engines off (cold). +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffCold() + self:SetTakeoff(SPAWN.Takeoff.Cold) + return self +end + +--- Set takeoff in air near the carrier. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffAir() + self:SetTakeoff(SPAWN.Takeoff.Air) + return self +end + + +--- Check if tanker is returning to base. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is returning to base. +function RESCUEHELO:IsReturning() + return self:is("Returning") +end + +--- Check if tanker is operating. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is operating. +function RESCUEHELO:IsRunning() + return self:is("Running") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #RESCUEHELO self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO:onafterStart(From, Event, To) + + -- Events are handled my MOOSE. + self:I(string.format("Starting Rescue Helo Formation v%s for carrier unit %s of type %s.", RESCUEHELO.version, self.carrier:GetName(), self.carriertype)) + + -- Handle events. + --self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Land) + --self:HandleEvent(EVENTS.Crash) + + -- Offset [meters] in the direction of travelling. Positive values are in front of Mother. + local OffsetX=200 + -- Offset [meters] perpendicular to travelling. Positive = Starboard (right of Mother), negative = Port (left of Mother). + local OffsetZ=200 + -- Offset altitude. Should (obviously) always be positve. + local OffsetY=70 + + -- Delay before formation is started. + local delay=120 + + -- Spawn helo. + local Spawn=SPAWN:New(self.helogroupname):InitUnControlled(false) + + -- Spawn in air or at airbase. + if self.takeoff==SPAWN.Takeoff.Air then + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Spawn distance behind carrier. + local dist=UTILS.NMToMeters(0.2) + + -- Coordinate behind the carrier + local Carrier=self.carrier:GetCoordinate():SetAltitude(OffsetY):Translate(dist, hdg) + + -- Orientation of spawned group. + Spawn:InitHeading(hdg) + + -- Spawn at coordinate. + self.helo=Spawn:SpawnFromCoordinate(Carrier) + + -- Start formation in 1 seconds + delay=1 + + else + + -- Spawn at airbase. + self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) + + if self.takeoff==SPAWN.Takeoff.Runway then + delay=5 + elseif self.takeoff==SPAWN.Takeoff.Hot then + delay=30 + elseif self.takeoff==SPAWN.Takeoff.Cold then + delay=60 + end + + end + + -- Set of group(s) to follow Mother. + self.followset=SET_GROUP:New() + self.followset:AddGroup(self.helo) + + -- Get initial fuel. + self.HeloFuel0=self.helo:GetFuel() + + -- Define AI Formation object. + self.formation=AI_FORMATION:New(self.carrier, self.followset, "Helo Formation with Carrier", "Follow Carrier at given parameters.") + + -- Formation parameters. + self.formation:FormationCenterWing(-OffsetX, 50, math.abs(OffsetY), 50, OffsetZ, 50) + + -- Start formation FSM. + self.formation:__Start(delay) + + -- Start uncontrolled helo. + --HeloSpawn:StartUncontrolled(120) + + -- Init status check + self:__Status(1) + +end + +--- On after Status event. Checks player status. +-- @param #RESCUEHELO self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Get relative fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712) + local fuel=self.helo:GetFuel()/self.HeloFuel0*100 + + -- Report current fuel. + local text=string.format("Rescue Helo %s: state=%s fuel=%.1f", self.helo:GetName(), self:GetState(), fuel) + self:I(text) + + -- If fuel < threshold ==> send helo to home base! + if fuel Date: Sun, 18 Nov 2018 00:45:18 +0100 Subject: [PATCH 046/147] AIRBOSS v0.2.8 good commit, improved and fixed a lot of stuff. --- Moose Development/Moose/Core/Radio.lua | 97 +-- Moose Development/Moose/Ops/Airboss.lua | 699 +++++++++++------- .../Moose/Ops/RecoveryTanker.lua | 3 +- Moose Development/Moose/Ops/RescueHelo.lua | 56 +- 4 files changed, 538 insertions(+), 317 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index d46b95310..4b7908353 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -76,7 +76,7 @@ -- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. -- -- @type RADIO --- @field Positionable#POSITIONABLE Positionable The transmiter. +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will transmit the radio calls. -- @field #string FileName Name of the sound file played. -- @field #number Frequency Frequency of the transmission in Hz. -- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM). @@ -93,7 +93,7 @@ RADIO = { Subtitle = "", SubtitleDuration = 0, Power = 100, - Loop = true, + Loop = false, } --- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast. @@ -102,9 +102,9 @@ RADIO = { -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. -- @return #RADIO The RADIO object or #nil if Positionable is invalid. function RADIO:New(Positionable) + + -- Inherit base local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO - - self.Loop = true -- default Loop to true (not sure the above RADIO definition actually is working) self:F(Positionable) if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid @@ -155,18 +155,23 @@ function RADIO:SetFrequency(Frequency) -- Convert frequency from MHz to Hz self.Frequency = Frequency * 1000000 - local commandSetFrequency={ - id = "SetFrequency", - params = { - frequency = self.Frequency, - modulation = self.Modulation, - } - } + -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then + + local commandSetFrequency={ + id = "SetFrequency", + params = { + frequency = self.Frequency, + modulation = self.Modulation, + } + } + + self:I(commandSetFrequency) self.Positionable:SetCommand(commandSetFrequency) end + return self end end @@ -280,28 +285,28 @@ end -- but it will work for any @{Wrapper.Positionable#POSITIONABLE}. -- Only the RADIO and the Filename are mandatory. -- @param #RADIO self --- @param #string FileName --- @param #string Subtitle --- @param #number SubtitleDuration in s --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #boolean Loop +-- @param #string FileName Name of sound file. +-- @param #string Subtitle Subtitle to be displayed with sound file. +-- @param #number SubtitleDuration Duration of subtitle display in seconds. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation which can be either radio.modulation.AM or radio.modulation.FM +-- @param #boolean Loop If true, loop message. -- @return #RADIO self function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop) - self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) + self:E({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) -- Set file name. self:SetFileName(FileName) - -- Set frequency. - if Frequency then - self:SetFrequency(Frequency) - end - -- Set modulation AM/FM. if Modulation then self:SetModulation(Modulation) end + + -- Set frequency. + if Frequency then + self:SetFrequency(Frequency) + end -- Set subtitle. if Subtitle then @@ -316,7 +321,7 @@ function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequen return self end ---- Actually Broadcast the transmission +--- Broadcast the transmission. -- * The Radio has to be populated with the new transmission before broadcasting. -- * Please use RADIO setters or either @{#RADIO.NewGenericTransmission} or @{#RADIO.NewUnitTransmission} -- * This class is in fact pretty smart, it determines the right DCS function to use depending on the type of POSITIONABLE @@ -325,35 +330,33 @@ end -- * If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored. -- * If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration are ignored -- @param #RADIO self --- @param #string filename (Optinal) Sound file name. Default self.FileName. --- @param #string subtitle (Optional) Subtitle. Default self.Subtitle. --- @param #number subtitleduraction (Optional) Subtitle duraction. Default self.SubtitleDuration. +-- @param #boolean trigger Use trigger.action.radioTransmission() in any case, i.e. also for UNITS and GROUPS. -- @return #RADIO self -function RADIO:Broadcast(filename, subtitle, subtitleduration) - self:F() +function RADIO:Broadcast(viatrigger) + self:F({viatrigger=viatrigger}) - filename=filename or self.FileName - subtitle=subtitle or self.Subtitle - subtitleduration=subtitleduration or self.SubtitleDuration - - -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self:T2("Broadcasting from a UNIT or a GROUP") - self.Positionable:SetCommand({ + -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system. + if (self.Positionable.ClassName=="UNIT" or self.Positionable.ClassName=="GROUP") and (not viatrigger) then + self:I("Broadcasting from a UNIT or a GROUP") + + local commandTransmitMessage={ id = "TransmitMessage", params = { - file = filename, - duration = subtitleduration, - subtitle = subtitle, + file = self.FileName, + duration = self.SubtitleDuration, + subtitle = self.Subtitle, loop = self.Loop, - } - }) + }} + + self:I(commandTransmitMessage) + self.Positionable:SetCommand(commandTransmitMessage) else -- If the POSITIONABLE is anything else, we revert to the general singleton function -- I need to give it a unique name, so that the transmission can be stopped later. I use the class ID - self:T2("Broadcasting from a POSITIONABLE") + self:I("Broadcasting from a POSITIONABLE") trigger.action.radioTransmission(self.FileName, self.Positionable:GetPositionVec3(), self.Modulation, self.Loop, self.Frequency, self.Power, tostring(self.ID)) end + return self end @@ -367,10 +370,10 @@ function RADIO:StopBroadcast() self:F() -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ - id = "StopTransmission", - params = {} - }) + + local commandStopTransmission={id="StopTransmission", params={}} + + self.Positionable:SetCommand(commandStopTransmission) else -- Else, we use the appropriate singleton funciton trigger.action.stopRadioTransmission(tostring(self.ID)) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 85eee83cc..1155d6813 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -17,7 +17,7 @@ -- * Multiple carriers supported. -- -- **PLEASE NOTE** 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. +-- At the moment training parameters are optimized for F/A-18C Hornet as aircraft and USS John C. Stennis as carrier. -- Other aircraft and carriers **might** be possible in future but would need a different set of parameters. -- -- === @@ -67,8 +67,8 @@ -- @field #table flights List of all flights in the CCA. -- @field #table Qmarshal Queue of marshalling aircraft groups. -- @field #table Qpattern Queue of aircraft groups in the landing pattern. --- @field #RESCUEHELO rescuehelo Rescue helo flying in close formation with the carrier. --- @field #RECOVERYTANKER tanker Refuelling tanker flying overhead with the carrier. +-- @field Ops.RescueHelo#RESCUEHELO rescuehelo Rescue helo flying in close formation with the carrier. +-- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. -- @field #table recoverytime List of time intervals when aircraft are recovered. -- @extends Core.Fsm#FSM @@ -128,15 +128,33 @@ AIRBOSS = { recoverytime = {}, } ---- Aircraft types. --- @type AIRBOSS.AircraftType +--- Player aircraft types capable of landing on carriers. +-- @type AIRBOSS.AircraftPlayer -- @field #string AV8B AV-8B Night Harrier. -- @field #string HORNET F/A-18C Lot 20 Hornet. -AIRBOSS.AircraftType={ +AIRBOSS.AircraftPlayer={ AV8B="AV8BNA", HORNET="FA-18C_hornet", } +--- Aircraft types capable of landing on carrier (human+AI). +-- @type AIRBOSS.AircraftCarrier +-- @field #string S3B Lockheed S-3B Viking. +-- @field #string S3BTANKER Lockheed S-3B Viking tanker. +-- @field #string E2D Grumman E-2D Hawkeye AWACS. +-- @field #string FA18C F/A-18C Hornet (AI). +-- @field #string F14A F-14A (AI). +AIRBOSS.AircraftCarrier={ + AV8B="AV8BNA", + HORNET="FA-18C_hornet", + S3B="S-3B", + S3BTANKER="S-3B Tanker", + E2D="E-2C", + FA18C="F/A-18C", + F14A="F-14A", +} + + --- Carrier types. -- @type AIRBOSS.CarrierType -- @field #string STENNIS USS John C. Stennis (CVN-74) @@ -190,7 +208,7 @@ AIRBOSS.PatternStep={ --- Radio sound file and subtitle. -- @type AIRBOSS.RadioSound -- @field #string normal Sound file normal. --- @field #string loud Sound file loud. +-- @field #string louder Sound file loud. -- @field #string subtitle Subtitle displayed during transmission. -- @field #number duration Duration in seconds the subtitle is displayed. @@ -222,30 +240,43 @@ AIRBOSS.PatternStep={ AIRBOSS.Soundfile={ RIGHTFORLINEUP={ normal="LSO - RightLineUp(S).ogg", - loud="LSO - RightLineUp(L).ogg", + louder="LSO - RightLineUp(L).ogg", subtitle="Right for line up.", duration=3, }, COMELEFT={ normal="LSO - ComeLeft(S).ogg", - loud="LSO - ComeLeft(L).ogg", + louder="LSO - ComeLeft(L).ogg", subtitle="Come left.", duration=3, }, HIGH={ normal="LSO - High(S).ogg", - loud="LSO - High(L).ogg", + louder="LSO - High(L).ogg", subtitle="You're high.", duration=3, }, POWER={ normal="LSO - Power(S).ogg", - loud="LSO - Power(L).ogg", + louder="LSO - Power(L).ogg", subtitle="Power.", duration=3, }, + SLOW={ + normal="LSO-Slow-Normal.ogg", + louder="LSO-Slow-Loud.ogg", + subtitle="You're slow.", + duration=3, + }, + FAST={ + normal="LSO-Fast-Normal.ogg", + louder="LSO-Fast-Loud.ogg", + subtitle="You're fast.", + duration=3, + }, CALLTHEBALL={ normal="LSO - Call the Ball.ogg", + louder="LSO - Call the Ball.ogg", subtitle="Call the ball.", duration=3, }, @@ -371,12 +402,14 @@ AIRBOSS.GroovePos={ -- @field Wrapper.Group#GROUP group Flight group. -- @field #string groupname Name of the group. -- @field #number nunits Number of units in group. --- @field #number stack Altitude in feet. +-- @field #number dist0 Distance to carrier in meters when the group was first detected inside the CCA. -- @field #number fuel Fuel state. -- @field #number time Time the flight was added to the queue. -- @field Core.UserFlag#USERFLAG flag User flag for triggering events for the flight. -- @field #boolean ai If true, flight is AI. If false, flight is a human player. -- @field #AIRBOSS.PlayerData player Player data for human pilots. +-- @field #string actype Aircraft type name. +-- @field #table onboardnumbers Onboard numbers of aircraft in the group. --- Main radio menu. -- @field #table MenuF10 @@ -384,12 +417,14 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.2.7w" +AIRBOSS.version="0.2.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Add radio transmission queue for LSO and airboss. +-- TODO: Get correct wire when trapped. -- TODO: Add radio check (LSO, AIRBOSS) to F10 radio menu. -- TODO: Monitor holding of players/AI in zoneHolding. -- TODO: Right pattern step after bolter/wo/patternWO? @@ -405,7 +440,7 @@ AIRBOSS.version="0.2.7w" -- TODO: CASE III. -- TODO: Foul deck check. -- TODO: Persistence of results. --- TODO: Stike group with helo bringing cargo. +-- TODO: Strike group with helo bringing cargo etc. -- DONE: Add scoring to radio menu. -- DONE: Optimized debrief. -- DONE: Add automatic grading. @@ -415,7 +450,7 @@ AIRBOSS.version="0.2.7w" -- Constructor ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Create new AIRBOSS class object. +--- Create a new AIRBOSS class object for a specific aircraft carrier unit. -- @param #AIRBOSS 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. @@ -489,14 +524,18 @@ function AIRBOSS:New(carriername, alias) -- Default recovery case. self:SetRecoveryCase(1) - -- Debug. - env.info("FF sound files:") + -- Init default sound files. for _name,_sound in pairs(AIRBOSS.Soundfile) do local sound=_sound --#AIRBOSS.RadioSound - self:I{name=_name,sound=_sound} self.radiocall[_name]=sound end + -- Debug: + self:T(self.lid.."Default sound files:") + for _name,_sound in pairs(self.radiocall) do + self:T{name=_name,sound=_sound} + end + ----------------------- --- FSM Transitions --- ----------------------- @@ -506,10 +545,11 @@ function AIRBOSS:New(carriername, alias) -- Add FSM transitions. -- From State --> Event --> To State - self:AddTransition("Stopped", "Start", "Running") - self:AddTransition("Running", "Recover", "Recovering") -- Recover aircraft. - self:AddTransition("*", "Status", "*") - self:AddTransition("*", "Stop", "Stopped") + self:AddTransition("Stopped", "Start", "Idle") -- Start AIRBOSS script. + self:AddTransition("*", "Idle", "Idle") -- Carrier is idleing. + self:AddTransition("Idle", "Recover", "Recovering") -- Recover aircraft. + self:AddTransition("*", "Status", "*") -- Update status of players and queues. + self:AddTransition("*", "Stop", "Stopped") -- Stop AIRBOSS script. --- Triggers the FSM event "Start" that starts the airboss. Initializes parameters and starts event handlers. @@ -522,12 +562,22 @@ function AIRBOSS:New(carriername, alias) -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Idle" so no operations are carried out. + -- @function [parent=#AIRBOSS] Idle + -- @param #AIRBOSS self + + --- Triggers the FSM event "Idle" after a delay. + -- @function [parent=#AIRBOSS] __Idle + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Recover" that starts the recovering of aircraft. Marshalling aircraft are send to the landing pattern. -- @function [parent=#AIRBOSS] Recover -- @param #AIRBOSS self --- Triggers the FSM event "Recover" that starts the recovering of aircraft after a delay. Marshalling aircraft are send to the landing pattern. - -- @function [parent=#AIRBOSS] __Start + -- @function [parent=#AIRBOSS] __Recover -- @param #AIRBOSS self -- @param #number delay Delay in seconds. @@ -631,7 +681,7 @@ function AIRBOSS:SetICLS(channel) end ---- Set LSO radio frequency. +--- Set LSO radio frequency and modulation. Default frequency is 264 MHz AM. -- @param #AIRBOSS self -- @param #number frequency Frequency in MHz. Default 264 MHz. -- @param #string modulation Modulation, i.e. "AM" (default) or "FM". @@ -653,7 +703,7 @@ function AIRBOSS:SetLSOradio(frequency, modulation) return self end ---- Set carrier radio frequency. +--- Set carrier radio frequency and modulation. Default frequency is 305 MHz AM. -- @param #AIRBOSS self -- @param #number frequency Frequency in MHz. Default 305 MHz. -- @param #string modulation Modulation, i.e. "AM" (default) or "FM". @@ -683,11 +733,11 @@ function AIRBOSS:IsRecovering() return self:is("Recovering") end ---- Check if carrier is operating. +--- Check if carrier is idle, i.e. no operations are carried out. -- @param #AIRBOSS self --- @return #boolean If true, helo is operating. -function AIRBOSS:IsRunning() - return self:is("Running") +-- @return #boolean If true, carrier is in idle state. +function AIRBOSS:IsIdle() + return self:is("Idle") end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -704,6 +754,10 @@ function AIRBOSS: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.", AIRBOSS.version, self.carrier:GetName(), self.carriertype)) + local theatre=env.mission.theatre + + self:I(self.lid..string.format("Theatre = %s", tostring(theatre))) + -- Activate TACAN. if self.TACANchannel~=nil and self.TACANmode~=nil then self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, "STN", true) @@ -737,9 +791,11 @@ function AIRBOSS:onafterStatus(From, Event, To) local time=timer.getTime() -- Check if we go into recovery mode. - local startrecovery=self:_CheckRecoveryTimes() - if startrecovery==true then + local recovery=self:_CheckRecoveryTimes() + if recovery==true then self:Recover() + elseif recovery==false then + self:Idle() end -- Update marshal and pattern queue every 30 seconds. @@ -770,6 +826,7 @@ end -- @return #boolean IF true, start recovery. function AIRBOSS:_CheckRecoveryTimes() + -- Get current abs time. local abstime=timer.getAbsTime() if #self.recoverytime==0 then @@ -777,33 +834,59 @@ function AIRBOSS:_CheckRecoveryTimes() -- If no recovery times have been specified, we assume any time is okay. self:I("FF Start recovery. No recovery time set!") if not self:IsRecovering() then + -- Give command to recover! return true else + -- Do nothing. return nil end else local recovery=false + local remove={} - for _,_rtime in pairs(self.recoverytime) do + for i,_rtime in pairs(self.recoverytime) do local rtime=_rtime --#AIRBOSS.Recovery if abstime>=rtime.START and abstime<=rtime.STOP then - - if not self:IsRecovering() then - self:I("FF Start recovery.") - return true - else - -- Nothing to do. Return nil. - return nil - end + + -- This is a valid time slot. Do not touch recovery again! + recovery=true + elseif abstime>rtime.STOP then + -- Stop time has already passed. + table.insert(remove, i) + elseif abstime0 then local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.Flightitem - Tpattern=timer.getTime()-patternflight.time - env.info(string.format("Pattern time of group %s = %d seconds", patternflight.groupname, Tpattern)) + Tpattern=timer.getAbsTime()-patternflight.time + self:I(self.lid..string.format("Pattern time of group %s = %d seconds", patternflight.groupname, Tpattern)) end + -- Min time in pattern before next aircraft is allowed. local TpatternMin=120 if self.case==1 then TpatternMin=45 end + -- Min time in marshal before send to landing pattern. local TmarshalMin=120 -- Two minutes in pattern at leastand >45 sec interval between pattern flights. @@ -1097,89 +1179,57 @@ function AIRBOSS:_PrintQueue(queue, name) for i,_flight in pairs(queue) do local flight=_flight --#AIRBOSS.Flightitem local clock=UTILS.SecondsToClock(flight.time) - text=text..string.format("\n[%d] %s*%d: stack=%d, flag=%d time=%s", i, flight.groupname, flight.nunits, flight.stack, flight.flag:Get(), clock) + -- TODO: add stack alt from flag + local stack=flight.flag:Get() + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) + local fuel=flight.group:GetFuelMin()*100 + local ai=tostring(flight.ai) + text=text..string.format("\n[%d] %s*%d: alt=%d ft, stack(flag)=%d, time=%s, fuel=%d, ai=%s", i, flight.groupname, flight.nunits, alt, stack, clock, fuel, ai) end end - env.info(text) + self:I(self.lid..text) +end + +--- Get carrier coaltion. +-- @param #AIRBOSS self +function AIRBOSS:GetCoalition() + return self.carrier:GetCoalition() +end + +--- Get carrier coordinate. +-- @param #AIRBOSS self +function AIRBOSS:GetCoordinate() + return self.carrier:GetCoordinate() end --- Scan carrier zone for (new) units. -- @param #AIRBOSS self function AIRBOSS:_ScanCarrierZone() - --env.info("FF Scanning Carrier Zone") + self:T(self.lid.."Scanning Carrier Zone") -- Carrier position. - local coord=self.carrier:GetCoordinate() + local coord=self:GetCoordinate() local Rout=UTILS.NMToMeters(50) - local Rin=UTILS.NMToMeters(10) -- Scan units in carrier zone. local _,_,_,unitscan=coord:ScanObjects(Rout, true, false, false) - --[[ - -- Inside and outside zones. - local zbig=ZONE_RADIUS:New("Bla1", self.carrier:GetVec2(), Rout) - local zsma=ZONE_RADIUS:New("Bla2", self.carrier:GetVec2(), Rin) - - local firstscan=self.unitsout==nil - - -- Check if we scanned already. - if self.unitsout~=nil then - - for _,_unit in pairs(self.unitsout) do - local unit=_unit --Wrapper.Unit#UNIT - - -- Check if this an aircraft and that it is airborn and closing in. - if unit:IsAir() and unit:InAir() and unit:IsInZone(zsma)then - -- TODO: check for correct aircraft types and also helos! - -- TODO: check for right coalition. - - local group=unit:GetGroup() - local unitname=unit:GetName() - local groupname=group:GetName() - - local text=string.format("In carrier zone: unit=%s group=%s", unitname, groupname) - --env.info(text) - - -- Check that it is not already in one of the queues. - if not (self:_InQueue(self.Qmarshal, group) or self:_InQueue(self.Qpattern, group)) then - - - if self:_IsHuman(group) then - env.info("FF new HUMAN marshal group (not used, register manually!)="..groupname) - --self:_MarshalPlayer(group) - else - env.info("FF new AI marshal group="..groupname) - self:_MarshalAI(group) - end - end - end - end - - end - - -- Get all air(born) units that are currently outside but not inside. - self.unitsout={} - for _,_unit in pairs(unitscan) do - local unit=_unit --Wrapper.Unit#UNIT - if unit:IsAir() and unit:InAir() and unit:IsInZone(zbig) and not unit:IsInZone(zsma) then - env.info(string.format("Possible incoming unit %s", unit:GetName())) - table.insert(self.unitsout, unit) - end - end - - ]] - - -- Make a table with all groups currently in the zone. + -- Make a table with all groups currently in the CCA zone. local insideCCA={} for _,_unit in pairs(unitscan) do local unit=_unit --Wrapper.Unit#UNIT + -- Necessary conditions to be met: + local airborn=unit:IsAir() and unit:InAir() + local inzone=unit:IsInZone(self.zoneCCA) + local friendly=self:GetCoalition()==unit:GetCoalition() + local carrierac=self:_IsCarrierAircraft(unit) + -- Check if this an aircraft and that it is airborn and closing in. - if unit:IsAir() and unit:InAir() and unit:IsInZone(self.zoneCCA)then + if airborn and inzone and friendly and carrierac then local group=unit:GetGroup() local groupname=group:GetName() @@ -1190,34 +1240,45 @@ function AIRBOSS:_ScanCarrierZone() end end + -- Find new flights that are inside CCA. for groupname,_group in pairs(insideCCA) do local group=_group --Wrapper.Group#GROUP - -- Loop over all known flight groups. - local known=false - for _,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.Flightitem - if flight.groupname==groupname then - known=true - break - end - end + -- Get flight group if possible. + local knownflight=self:_GetFlightFromGroupInQueue(group, self.flights) + + -- Get aircraft type name. + local actype=group:GetTypeName() -- Create a new flight group - if not known then + if knownflight then + self:I(string.format("Known CCA flight group %s of type %s", groupname, actype)) + if knownflight.ai then + + -- Get distance to carrier. + local dist=knownflight.group:GetCoordinate():Get2DDistance(self:GetCoordinate()) + + -- Send AI flight to marshal stack if group closes in more than 5 km and has initial flag value. + if knownflight.dist0-dist>5000 and knownflight.flag:Get()==-100 then + self:_MarshalAI(knownflight) + end + end + else + self:I(string.format("UNKNOWN CCA flight group %s of type %s", groupname, actype)) self:_CreateFlightGroup(group) end end + -- Find flights that are not in CCA. local remove={} for _,_flight in pairs(self.flights) do local flight=_flight --#AIRBOSS.Flightitem if insideCCA[flight.groupname]==nil then - table.insert(remove, flight.group) + table.insert(remove, flight.group) end end @@ -1228,6 +1289,43 @@ function AIRBOSS:_ScanCarrierZone() end +--- Get onboard numbers of all units in a group. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #table Table of onboard numbers. +function AIRBOSS:_GetOnboardNumbers(group) + --self:F({groupname=group:GetName}) + + -- Get group name. + local groupname=group:GetName() + + -- Debug text. + local text=string.format("Onboard numbers of group %s:", groupname) + + -- Units of template group. + local units=group:GetTemplate().units + + -- Get numbers. + local numbers={} + for _,unit in pairs(units) do + + -- Onboard number and unit name. + local n=tostring(unit.onboard_num) + local name=unit.name + + -- Table entry. + numbers[name]=n + + -- Debug text. + text=text..string.format("\n- unit=%s - onboard #=%s", name, n) + end + + -- Debug info. + self:I(self.lid..text) + + return numbers +end + --- Create a new flight group. Usually when a flight appears in the CCA. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. @@ -1239,29 +1337,36 @@ function AIRBOSS:_CreateFlightGroup(group) local human=self:_IsHuman(group) -- Queue table item. - local qitem={} --#AIRBOSS.Flightitem - qitem.group=group - qitem.groupname=group:GetName() - qitem.nunits=#group:GetUnits() - qitem.fuel=group:GetFuelMin() - qitem.time=timer.getAbsTime() - qitem.flag=USERFLAG:New(groupname) - qitem.flag:Set(-100) - qitem.ai=not human + local flight={} --#AIRBOSS.Flightitem + flight.group=group + flight.groupname=group:GetName() + flight.nunits=#group:GetUnits() + flight.fuel=group:GetFuelMin() + flight.time=timer.getAbsTime() + flight.dist0=group:GetCoordinate():Get2DDistance(self:GetCoordinate()) + flight.flag=USERFLAG:New(groupname) + flight.flag:Set(-100) + flight.ai=not human + flight.actype=group:GetTypeName() + flight.onboardnumbers=self:_GetOnboardNumbers(group) if human then - - local playerData=self:_GetPlayerDataGroup(group) - qitem.player=playerData + + -- Attach player data to flight. + local playerData=self:_GetPlayerDataGroup(group) + flight.player=playerData else -- Send AI to holding pattern. - self:_MarshalAI(qitem) + --self:_MarshalAI(flight) end + + -- Add to known flights inside CCA zone. + table.insert(self.flights, flight) - return qitem + return flight end --- Remove a flight group. @@ -1291,15 +1396,21 @@ function AIRBOSS:_MarshalPlayer(group) -- Number of full marshal stacks. local nstacks=#self.Qmarshal + -- Get player data. local playerData=self:_GetPlayerDataGroup(group) - if playerData then - --self:_SendMessageToPlayer(message,duration,playerData,clear,sender,delay) + + local knownflight=self:_GetFlightFromGroupInQueue(group, self.flights) + + -- Check if flight is known to the airboss already. + if playerData and knownflight then + -- Add group to marshal stack. + self:_AddMarshallGroup(knownflight, nstacks+1) + else + -- Flight is not registered yet. + local text="You are not yet registered inside the CCA. Marshal request denied!" + self:_SendMessageToPlayer(text, 30, playerData) end - -- Add group to marshal stack. - self:_AddMarshallGroup(group, nstacks+1) - - --TODO: playerData set end --- Tell AI to orbit at a specified position at a specified alititude with a specified speed. @@ -1315,7 +1426,7 @@ function AIRBOSS:_MarshalAI(flight) local nstacks=#self.Qmarshal -- Current carrier position. - local Carrier=self.carrier:GetCoordinate() + local Carrier=self:GetCoordinate() -- Aircraft speed when flying the pattern. local Speed=UTILS.KnotsToMps(272) @@ -1340,9 +1451,12 @@ function AIRBOSS:_MarshalAI(flight) -- Get altitude and positions. local Altitude, p1, p2=self:_GetMarshalAltitude(stack) + local p1=p1 --Core.Point#COORDINATE + local Dist=p1:Get2DDistance(self:GetCoordinate()) + -- Orbit task. local TaskOrbit=_taskorbit(p1, Altitude, Speed, stack-1, p2) - + -- Waypoint description. local text=string.format("Marshal @ alt=%d ft, dist=%.1f NM, speed=%d knots", UTILS.MetersToFeet(Altitude), UTILS.MetersToNM(Dist), UTILS.MpsToKnots(Speed)) @@ -1375,7 +1489,7 @@ end function AIRBOSS:_GetMarshalAltitude(stack) -- Carrier position. - local Carrier=self.carrier:GetCoordinate() + local Carrier=self:GetCoordinate() -- Altitude of first stack. Depends on recovery case. local angels0 @@ -1386,8 +1500,8 @@ function AIRBOSS:_GetMarshalAltitude(stack) if self.case==1 then -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next stack. angels0=2 - Dist=UTILS.NMToMeters(5) - p1=Carrier:Translate(Dist, 270) + Dist=UTILS.NMToMeters(2.5) + p1=Carrier:Translate(Dist, 280) else -- CASE III: Holding at 6000 ft on a racetrack pattern astern the carrier. angels0=6 @@ -1413,15 +1527,15 @@ function AIRBOSS:_AddMarshallGroup(flight, flagvalue) -- Pressure. local hPa2inHg=0.0295299830714 - local P=self.carrier:GetCoordinate():GetPressure()*hPa2inHg + local P=self:GetCoordinate():GetPressure()*hPa2inHg -- TODO: Get correct board number if possible? local boardnumber=flight.groupname - local alt=self:_GetMarshalAltitude(flagvalue) + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(flagvalue)) local brc=self:_BaseRecoveryCourse() -- Marshal message. - local text=string.format("%s, Case 1, BRC is %03d, hold at %d. Expected Charlie Time XX.\n", boardnumber, brc, alt) + local text=string.format("%s, Case %d, BRC is %03d, hold at %d. Expected Charlie Time XX.\n", boardnumber, self.case, brc, alt) text=text..string.format("Altimeter %.2f. Report see me.", P) MESSAGE:New(text, 30):ToAll() @@ -1433,12 +1547,14 @@ end -- @param #AIRBOSS self function AIRBOSS:_CollapseMarshalStack() + -- Decrease flag values of all groups in marshal stack. for _,_flight in pairs(self.Qmarshal) do local flight=_flight --#AIRBOSS.Flightitem local flagvalue=flight.flag:Get() flight.flag:Set(flagvalue-1) end + -- Number of marshal flight groups. local nmarshal=#self.Qmarshal for i=nmarshal,1,-1 do @@ -1446,21 +1562,22 @@ function AIRBOSS:_CollapseMarshalStack() --flight. end + -- First flight to enter the landing pattern. local flight=self.Qmarshal[1] --#AIRBOSS.Flightitem - - env.info(string.format("New pattern flight %s.", flight.groupname)) + + self:I(self..lid..string.format("New pattern flight %s.", flight.groupname)) -- TODO: better message. MESSAGE:New(string.format("Marshal, %s, you are cleared for Case I recovery pattern!", flight.groupname), 15):ToAll() - -- Set player step to 0. + -- Set player step. if flight.ai==false then local playerData=self:_GetPlayerDataGroup(flight.group) playerData.step=AIRBOSS.PatternStep.COMMENCING end - -- Time stamp. - flight.time=timer.getTime() + -- New time stamp for time in pattern. + flight.time=timer.getAbsTime() -- Add flight to pattern queue table.insert(self.Qpattern, flight) @@ -1481,14 +1598,38 @@ function AIRBOSS:_RemoveGroupFromQueue(queue, group) local flight=_flight --#AIRBOSS.Flightitem if flight.groupname==name then - env.info(string.format("FF removing group %s from queue.", name)) + self:I(self.lid..string.format("Removing group %s from queue.", name)) table.remove(queue, i) end end end ---- Remove a group from a queue. +--- Get flight from group. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Group that will be removed from queue. +-- @param #table queue The queue from which the group will be removed. +-- @return #AIRBOSS.Flightitem Flight group. +-- @return #number Queue index. +function AIRBOSS:_GetFlightFromGroupInQueue(group, queue) + + -- Group name + local name=group:GetName() + + -- Loop over all flight groups in queue + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.Flightitem + + if flight.groupname==name then + return flight, i + end + end + + self:T2(self.lid..string.format("WARNING: Flight group %s could not be found in queue.", name)) + return nil, nil +end + +--- Remove a group from a queue when all aircraft of that group have landed. -- @param #AIRBOSS self -- @param #table queue The queue from which the group will be removed. -- @param Wrapper.Group#GROUP group Group that will be removed from queue. @@ -1501,10 +1642,11 @@ function AIRBOSS:_RemoveQueue(queue, group) if flight.groupname==name then + -- Decrease number of units in group. flight.nunits=flight.nunits-1 if flight.nunits==0 then - env.info(string.format("FF removing group %s from queue.", name)) + self:I(self.lid..string.format("FF removing group %s from queue.", name)) table.remove(queue, i) end @@ -1704,15 +1846,10 @@ function AIRBOSS:OnEventBirth(EventData) MESSAGE:New(text, 5):ToAllIf(self.Debug) - local rightaircraft=false - local aircraft=_unit:GetTypeName() - for _,actype in pairs(AIRBOSS.AircraftType) do - if actype==aircraft then - rightaircraft=true - end - end + -- Check if aircraft type the player occupies is carrier capable. + local rightaircraft=self:_IsCarrierAircraft(_unit) if rightaircraft==false then - self:E(string.format("Player aircraft %s not supported by AIRBOSS class.", aircraft)) + self:E(string.format("Player aircraft type %s not supported by AIRBOSS class.", _unit:GetTypeName())) return end @@ -1722,12 +1859,31 @@ function AIRBOSS:OnEventBirth(EventData) -- Init player data. self.players[_playername]=self:_NewPlayer(_unitName) + --env.info("FF radiocall LSO long in groove") + --self:RadioTransmission(self.LSOradio, self.radiocall["LONGINGROOVE"], false, 5) + --self:RadioTransmission(self.LSOradio, self.radiocall.LONGINGROOVE, false, 20) + -- Start in the groove for debugging. - self.groovedebug=true + self.groovedebug=false end end +--- Check if aircraft is capable of landing on an aircraft carrier. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.) +-- @return #boolean If true, aircraft can land on a carrier. +function AIRBOSS:_IsCarrierAircraft(unit) + local carrieraircraft=false + local aircrafttype=unit:GetTypeName() + for _,actype in pairs(AIRBOSS.AircraftCarrier) do + if actype==aircrafttype then + return true + end + end + return false +end + --- Airboss event handler for event land. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData @@ -1756,7 +1912,7 @@ function AIRBOSS:OnEventLand(EventData) -- Debug output. local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename) - self:T(self.lid..text) + self:I(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) -- Player data. @@ -1770,13 +1926,12 @@ function AIRBOSS:OnEventLand(EventData) coord:SmokeGreen() -- Debug marks of wires. - local w1=self.carrier:GetCoordinate():Translate(self.carrierparam.wire1, 0):MarkToAll("Wire 1") - local w2=self.carrier:GetCoordinate():Translate(self.carrierparam.wire2, 0):MarkToAll("Wire 2") - local w3=self.carrier:GetCoordinate():Translate(self.carrierparam.wire3, 0):MarkToAll("Wire 3") - local w4=self.carrier:GetCoordinate():Translate(self.carrierparam.wire4, 0):MarkToAll("Wire 4") + local w1=self:GetCoordinate():Translate(self.carrierparam.wire1, 0):MarkToAll("Wire 1") + local w2=self:GetCoordinate():Translate(self.carrierparam.wire2, 0):MarkToAll("Wire 2") + local w3=self:GetCoordinate():Translate(self.carrierparam.wire3, 0):MarkToAll("Wire 3") + local w4=self:GetCoordinate():Translate(self.carrierparam.wire4, 0):MarkToAll("Wire 4") -- We did land. - env.info("FF landed") playerData.landed=true -- Unkonwn step. @@ -1787,13 +1942,18 @@ function AIRBOSS:OnEventLand(EventData) end - -- Remove + else + + -- TODO: Get landing coodinates of AI hornet for perfect _OK_ 3-wire pass! + + -- AI: Decrease number of units in flight and remove group from pattern queue if all units landed. if self:_InQueue(self.Qpattern, EventData.IniGroup) then self:_RemoveQueue(self.Qpattern, EventData.IniGroup) - end + end + end - + end --- Airboss event handler for event crash. @@ -1808,9 +1968,13 @@ function AIRBOSS:OnEventCrash(EventData) self:I(self.lid.."CRASH: unit = "..tostring(EventData.IniUnitName)) self:I(self.lid.."CRASH: group = "..tostring(EventData.IniGroupName)) self:I(self.lid.."CARSH: player = "..tostring(_playername)) - - if _unit and _playername then + + -- TODO: Update queues! + if _unit and _playername then + self:I(self.lid.."Player %s crashed!",_playername) + else + self:I(self.lid.."AI unit %s crashed!", EventData.IniUnitName) end end @@ -1837,6 +2001,7 @@ function AIRBOSS:_NewPlayer(unitname) -- Player unit, client and callsign. playerData.unit = playerunit playerData.name = playername + playerData.group = playerunit:GetGroup() playerData.callsign = playerData.unit:GetCallsign() playerData.client = CLIENT:FindByName(unitname, nil, true) @@ -2344,8 +2509,6 @@ function AIRBOSS:_Final(playerData) local lineup=self:_Lineup(playerData)-self.carrierparam.rwyangle local roll=playerData.unit:GetRoll() - env.info(string.format("FF relhead=%d lineup=%d roll=%d", relhead, lineup, roll)) - if math.abs(lineup)<5 and math.abs(relhead)<10 then -- Get player altitude and AoA. @@ -2438,10 +2601,10 @@ function AIRBOSS:_Groove(playerData) -- LSO "Call the ball" call. -- TODO: take this out. - self:_SendMessageToPlayer("Call the ball.", 5, playerData) + --self:_SendMessageToPlayer("Call the ball.", 5, playerData) -- LSO radio call. - self:RadioTransmission(self.LSOradio, self.radiocall.LONGINGROOVE) + self:RadioTransmission(self.LSOradio, self.radiocall.CALLTHEBALL) playerData.Tlso=timer.getTime() -- Store data. @@ -2454,7 +2617,7 @@ function AIRBOSS:_Groove(playerData) -- Pilot: "Roger ball" call. -- TODO: take this out. - self:_SendMessageToPlayer("Roger ball!", 5, playerData) + --self:_SendMessageToPlayer("Roger ball!", 5, playerData) -- LSO radio call. self:RadioTransmission(self.LSOradio, self.radiocall.ROGERBALL) @@ -2470,7 +2633,7 @@ function AIRBOSS:_Groove(playerData) -- Debug. self:_SendMessageToPlayer("IM", 8, playerData) - env.info(string.format("FF IM=%d", rho)) + self:I(self.lid..string.format("FF IM=%d", rho)) -- Store data. playerData.groove.IM=groovedata @@ -2485,7 +2648,7 @@ function AIRBOSS:_Groove(playerData) -- Debug self:_SendMessageToPlayer("IC", 8, playerData) - env.info(string.format("FF IC=%d", rho)) + self:I(self.lid..string.format("FF IC=%d", rho)) -- Store data. playerData.groove.IC=groovedata @@ -2497,7 +2660,7 @@ function AIRBOSS:_Groove(playerData) if waveoff then -- Wave off player. - self:_SendMessageToPlayer("Wave off!", 10, playerData) + --self:_SendMessageToPlayer("Wave off!", 10, playerData) -- LSO radio call. self:RadioTransmission(self.LSOradio, self.radiocall.WAVEOFF) @@ -2518,7 +2681,7 @@ function AIRBOSS:_Groove(playerData) -- Debug. self:_SendMessageToPlayer("AR", 8, playerData) - env.info(string.format("FF AR=%d", rho)) + self:I(self.lid..string.format("FF AR=%d", rho)) -- Store data. playerData.groove.AR=groovedata @@ -2604,7 +2767,7 @@ end -- @param Core.Point#COORDINATE pos Position of aircraft on landing event. function AIRBOSS:_Trapped(playerData, pos) - env.info("FF TRAPPED") + self:I(self.lid.."FF TRAPPED") -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi = self:_GetDistances(pos) @@ -2634,7 +2797,7 @@ function AIRBOSS:_Trapped(playerData, pos) local text2=string.format("Distance X=%.1f meters resulted in a %d-wire estimate.", X, wire) MESSAGE:New(text,30):ToAllIf(self.Debug) - env.info(text2) + self:I(self.lid..text2) local hint = string.format("Trapped catching the %d-wire.", wire) self:_AddToSummary(playerData, "Recovered", hint) @@ -2667,7 +2830,7 @@ function AIRBOSS:_DetailedPlayerStatus(playerData) local pitch=unit:GetPitch() -- Distance to the boat. - local dist=playerData.unit:GetCoordinate():Get2DDistance(self.carrier:GetCoordinate()) + local dist=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) local dx,dz,rho,phi=self:_GetDistances(unit) -- Wind vector. @@ -2675,28 +2838,34 @@ function AIRBOSS:_DetailedPlayerStatus(playerData) -- Aircraft veloecity vector. local velo=unit:GetVelocityVec3() + local vabs=UTILS.VecNorm(velo) -- Relative heading Aircraft to Carrier. local relhead=self:_GetRelativeHeading(playerData.unit) - -- Output - local text=string.format("AoA=%.1f | Vx=%.1f Vy=%.1f Vz=%.1f\n", aoa, velo.x, velo.y, velo.z) - text=text..string.format("Pitch=%.1f° | Roll=%.1f° | Yaw=%.1f° | Climb=%.1f°\n", pitch, roll, yaw, unit:GetClimbAngle()) - text=text..string.format("Relheading=%.1f°\n", relhead) - text=text..string.format("Distance: X=%d m Z=%d m | R=%d m Phi=%.1f\n", dx, dz, rho, phi) - --TODO: step names for check if in groove - --[[ - if playerData.step>=90 and playerData.step<=99 then + -- Output + local text=string.format("Pattern step: %s\n", playerData.step) + text=text..string.format("AoA=%.1f | |V|=%.1f knots\n", aoa, UTILS.MpsToKnots(vabs)) + text=text..string.format("Vx=%.1f Vy=%.1f Vz=%.1f m/s\n", velo.x, velo.y, velo.z) + text=text..string.format("Pitch=%.1f° | Roll=%.1f° | Yaw=%.1f°\n", pitch, roll, yaw) + text=text..string.format("Climb Angle=%.1f°\n | Rate=%d ft/min\n", unit:GetClimbAngle(), velo.y*196.85) + text=text..string.format("R=%d NM | X=%d Z=%d m\n", UTILS.MetersToNM(rho), dx, dz) + text=text..string.format("Phi=%.1f° | Rel=%.1f°", phi, relhead) + -- If in the groove, provide line up and glide slope error. + if playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_RB or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then local lineup=self:_Lineup(playerData)-self.carrierparam.rwyangle local glideslope=self:_Glideslope(playerData)-3.5 - text=text..string.format("Lineup Error = %.1f°\n", lineup) - text=text..string.format("Glideslope Error = %.1f°\n", glideslope) + text=text..string.format("\nLU Error = %.1f° (line up)", lineup) + text=text..string.format("\nGS Error = %.1f° (glide slope)", glideslope) end - ]] - text=text..string.format("Current step: %s\n", playerData.step) + -- Wind --text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) - --text=text..string.format("rho=%.1f m phi=%.1f degrees\n", rho,phi) MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) end @@ -2712,7 +2881,7 @@ function AIRBOSS:_Glideslope(playerData) -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. local h=playerData.unit:GetAltitude()-self.carrierparam.deckheight - local x=math.abs(self.carrierparam.wire3-X) + local x=math.abs(self.carrierparam.wire3-X) --TODO: Check if carrier has wires later. local glideslope=math.atan(h/x) return math.deg(glideslope) @@ -2748,6 +2917,7 @@ end -- @param #boolean True If true, return true bearing. Otherwise (default) return magnetic bearing. -- @return #number BRC in degrees. function AIRBOSS:_BaseRecoveryCourse(True) + self:E({TrueBearing=True}) -- Current true heading of carrier. local hdg=self.carrier:GetHeading() @@ -2781,7 +2951,7 @@ function AIRBOSS:_FinalBearing(True) local brc=self:_BaseRecoveryCourse(True) -- Final baring = BRC including angled deck. - local fb=brc+self.rwyangle + local fb=brc+self.carrierparam.rwyangle -- Adjust negative values. if fb<0 then @@ -2916,23 +3086,19 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- Glideslope high/low calls. local text="" if glideslopeError>1 then - --text="You're high!" - --AIRBOSS.LSOcall.HIGHL:ToGroup(player) + -- "You're high!" self:RadioTransmission(self.LSOradio, self.radiocall.HIGH, true, delay) delay=delay+1.5 elseif glideslopeError>0.5 then - --text="You're a little high." - --AIRBOSS.LSOcall.HIGHS:ToGroup(player) + -- "You're a little high." self:RadioTransmission(self.LSOradio, self.radiocall.HIGH, false, delay) delay=delay+1.5 elseif glideslopeError<-1.0 then - --text="Power!" - --AIRBOSS.LSOcall.POWERL:ToGroup(player) + -- "Power!" self:RadioTransmission(self.LSOradio, self.radiocall.POWER, true, delay) delay=delay+1.5 elseif glideslopeError<-0.5 then - --text="You're a little low." - --AIRBOSS.LSOcall.POWERS:ToGroup(player) + -- "You're a little low." self:RadioTransmission(self.LSOradio, self.radiocall.POWER, false, delay) delay=delay+1.5 else @@ -2944,23 +3110,19 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- Lineup left/right calls. if lineupError<-3 then - --text=text.."Come left!" - --AIRBOSS.LSOcall.COMELEFTL:ToGroup(player, delay) + -- "Come left!" self:RadioTransmission(self.LSOradio, self.radiocall.COMELEFT, true, delay) delay=delay+1.5 elseif lineupError<-1 then - --text=text.."Come left." - --AIRBOSS.LSOcall.COMELEFTS:ToGroup(player, delay) + -- "Come left." self:RadioTransmission(self.LSOradio, self.radiocall.COMELEFT, false, delay) delay=delay+1.5 elseif lineupError>3 then - --text=text.."Right for lineup!" - --AIRBOSS.LSOcall.RIGHTFORLINEUPL:ToGroup(player, delay) + -- "Right for lineup!" self:RadioTransmission(self.LSOradio, self.radiocall.RIGHTFORLINEUP, true, delay) delay=delay+1.5 elseif lineupError>1 then - --text=text.."Right for lineup." - --AIRBOSS.LSOcall.RIGHTFORLINEUPS:ToGroup(player, delay) + -- "Right for lineup." self:RadioTransmission(self.LSOradio, self.radiocall.RIGHTFORLINEUP, false, delay) delay=delay+1.5 else @@ -2972,23 +3134,24 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- Get AoA. local aoa=playerData.unit:GetAoA() + -- TODO: Generalize AoA for other aircraft! if aoa>=9.3 then - --text=text.."Your're slow!" + -- "Your're slow!" self:RadioTransmission(self.LSOradio, self.radiocall.SLOW, true, delay) delay=delay+1.5 elseif aoa>=8.8 and aoa<9.3 then + -- "Your're a little slow." self:RadioTransmission(self.LSOradio, self.radiocall.SLOW, false, delay) - delay=delay+1.5 - --text=text.."Your're a little slow." + delay=delay+1.5 elseif aoa>=7.4 and aoa<8.8 then text=text.."You're on speed." elseif aoa>=6.9 and aoa<7.4 then - --text=text.."You're a little fast." + -- "You're a little fast." self:RadioTransmission(self.LSOradio, self.radiocall.FAST, false, delay) delay=delay+1.5 elseif aoa>=0 and aoa<6.9 then - text=text.."You're fast!" - self:RadioTransmission(self.LSOradio, self.radiocall.FALSE, true, delay) + -- "You're fast!" + self:RadioTransmission(self.LSOradio, self.radiocall.FAST, true, delay) delay=delay+1.5 else text=text.."Unknown AoA state." @@ -2997,7 +3160,7 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) text=text..string.format(" AoA = %.1f", aoa) -- LSO Message to player. - self:_SendMessageToPlayer(text, 5, playerData, false) + --self:_SendMessageToPlayer(text, 5, playerData, false) -- Set last time. playerData.Tlso=timer.getTime() @@ -3487,7 +3650,7 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. function AIRBOSS:_Debrief(playerData) - env.info("FF debrief") + self:F("Debriefing") -- Debriefing text. local text=string.format("Debriefing:\n") @@ -3550,25 +3713,49 @@ end -- @param #boolean loud If true, play loud sound file version. -- @param #number delay Delay in seconds, before the message is broadcasted. function AIRBOSS:RadioTransmission(radio, call, loud, delay) - self:F({radio=radio, call=call, loud=loud, delay=delay}) - - env.info("FF call = "..tostring(call)) + self:E({radio=radio, call=call, loud=loud, delay=delay}) - if delay==nil or delay and delay==0 then + if (delay==nil) or (delay and delay==0) then + + if call==nil then + self:E(self.lid.."ERROR: Radio call=nil!") + self:E({radio=radio}) + self:E({call=call}) + self:E({loud=loud}) + self:E({delay=delay}) + return + end - local filename=call.normal + local filename if loud then - filename=call.loud + filename=call.louder + else + filename=call.normal end -- New transmission. radio:NewUnitTransmission(filename, call.subtitle, call.duration, radio.Frequency/1000000, radio.Modulation, false) -- Broadcast message. - radio:Broadcast() + radio:Broadcast(true) + + -- Subtitle. + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + self:_SendMessageToPlayer(call.subtitle, call.duration, playerData) + end else + if call==nil then + self:E(self.lid.."ERROR: Radio call=nil!") + self:E({radio=radio}) + self:E({call=call}) + self:E({loud=loud}) + self:E({delay=delay}) + return + end + -- Scheduled transmission. SCHEDULER:New(nil, self.RadioTransmission, {self, radio, call, loud}, delay) end @@ -3590,7 +3777,7 @@ function AIRBOSS:_SendMessageToPlayer(message, duration, playerData, clear, send sender=sender or self.alias local text=string.format("%s, %s, %s", sender, playerData.callsign, message) - env.info(text) + self:I(self.lid..text) if delay>0 then SCHEDULER:New(nil,self._SendMessageToPlayer, {self, message, duration, playerData, clear, sender}, delay) @@ -3807,13 +3994,13 @@ function AIRBOSS:_RequestMarshal(_unitName) -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - + -- Check if we have a unit which is a player. if _unit and _playername then local playerData=self.players[_playername] --#AIRBOSS.PlayerData - + if playerData then - self:_MarshalPlayer(_unit:GetGroup()) + self:_MarshalPlayer(playerData.group) end end end @@ -3853,7 +4040,7 @@ function AIRBOSS:_DisplayPlayerGrades(_unitName) text=text..string.format("\nNo data available.") end - env.info("FF:\n"..text) + --env.info("FF:\n"..text) -- Send message. if playerData.client then @@ -3905,7 +4092,7 @@ function AIRBOSS:_DisplayScoreBoard(_unitName) i=i+1 end - env.info("FF:\n"..text) + --env.info("FF:\n"..text) -- Send message. if playerData.client then @@ -3963,12 +4150,9 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) local playerData=self.players[playername] --#AIRBOSS.PlayerData if playerData then - - -- Message text. - local text=string.format("%s info:\n", self.alias) - + -- Current coordinates. - local coord=self.carrier:GetCoordinate() + local coord=self:GetCoordinate() -- Carrier speed and heading. local carrierheading=self.carrier:GetHeading() @@ -3983,21 +4167,24 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) if self.ICLSchannel~=nil then icls=string.format("%d", self.ICLSchannel) end - - -- Message text + + -- Message text. + local text=string.format("%s info:\n", self.alias) text=text..string.format("Case %d Recovery\n", self.case) - text=text..string.format("BRC %d°\n", self:_BaseRecoveryCourse()) - text=text..string.format("FB %d°\n", self:_FinalBearing()) + text=text..string.format("BRC %03d°\n", self:_BaseRecoveryCourse()) + text=text..string.format("FB %03d°\n", self:_FinalBearing()) text=text..string.format("Speed %d kts\n", carrierspeed) text=text..string.format("Airboss radio %.3f MHz AM\n", self.Carrierfreq) --TODO: add modulation text=text..string.format("LSO radio %.3f MHz AM\n", self.LSOfreq) text=text..string.format("TACAN Channel %s\n", tacan) text=text..string.format("ICLS Channel %s\n", icls) + text=text..string.format("# A/C total %d\n", #self.flights) text=text..string.format("# A/C holding %d\n", #self.Qmarshal) - text=text..string.format("# A/C pattern %d", #self.Qpattern) - + text=text..string.format("# A/C pattern %d", #self.Qpattern) + self:T2(self.lid..text) + -- Send message. - self:_SendMessageToPlayer(text, 20, playerData) + self:_SendMessageToPlayer(text, 20, playerData, true) else self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) @@ -4024,7 +4211,7 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) local text="" -- Current coordinates. - local coord=self.carrier:GetCoordinate() + local coord=self:GetCoordinate() -- Get atmospheric data at carrier location. local T=coord:GetTemperature() @@ -4032,7 +4219,7 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) local Wd,Ws=coord:GetWind() -- Get Beaufort wind scale. - local Bn,Bd=UTILS.BeaufortScale(Ws) + local Bn,Bd=UTILS.BeaufortScale(Ws) local WD=string.format('%03d°', Wd) local Ts=string.format("%d°C",T) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 1af9412e2..f63e75d17 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -67,12 +67,13 @@ RECOVERYTANKER = { --- Class version. -- @field #string version -RECOVERYTANKER.version="0.9.0w" +RECOVERYTANKER.version="0.9.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Possibility to add already present/spawned aircraft, e.g. for warehouse. -- TODO: Write documenation. -- TODO: Smarter pattern update function. E.g. (small) zone around carrier. Only update position when carrier leaves zone or changes heading? -- TODO: Maybe rework pattern update implementation altogether to make it smoother. diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 0c638e004..6272d32e3 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -28,6 +28,9 @@ -- @field Core.Set#SET_GROUP followset Follow group set. -- @field AI.AI_Formation#AI_FORMATION formation AI_FORMATION object. -- @field #number lowfuel Low fuel threshold of helo in percent. +-- @field #number altitude Altitude of helo in meters. +-- @field #number offsetX Offset in meters to carrier in longitudinal direction. +-- @field #number offsetZ Offset in meters to carrier in latitudinal direction. -- @extends Core.Fsm#FSM --- Rescue Helo @@ -52,16 +55,20 @@ RESCUEHELO = { followset = nil, formation = nil, lowfuel = nil, + altitude = nil, + offsetX = nil, + offsetZ = nil, } --- Class version. -- @field #string version -RESCUEHELO.version="0.9.0w" +RESCUEHELO.version="0.9.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Possibility to add already present/spawned aircraft, e.g. for warehouse. -- TODO: Write documenation. -- TODO: Add rescue event when aircraft crashes. -- TODO: Make offset input parameter. @@ -98,7 +105,10 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- Init defaults. self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) self:SetTakeoffHot() - self:SetLowFuelThreshold(10) + self:SetLowFuelThreshold() + self:SetAltitude() + self:SetOffsetX() + self:SetOffsetZ() ----------------------- --- FSM Transitions --- @@ -152,10 +162,10 @@ end --- Set low fuel state of helo. When fuel is below this threshold, the helo will RTB or be respawned if takeoff type is in air. -- @param #RESCUEHELO self --- @param #number threshold Low fuel threshold in percent. Default 10. +-- @param #number threshold Low fuel threshold in percent. Default 5%. -- @return #RESCUEHELO self function RESCUEHELO:SetLowFuelThreshold(threshold) - self.lowfuel=threshold or 10 + self.lowfuel=threshold or 5 return self end @@ -201,6 +211,33 @@ function RESCUEHELO:SetTakeoffAir() return self end +--- Set altitude of helo. +-- @param #RESCUEHELO self +-- @param #number alt Altitude in meters. Default 70 m. +-- @return #RESCUEHELO self +function RESCUEHELO:SetAltitude(alt) + self.altitude=alt or 70 + return self +end + +--- Set latitudinal offset to carrier. +-- @param #RESCUEHELO self +-- @param #number distance Latitual offset distance in meters. Default 200 m. +-- @return #RESCUEHELO self +function RESCUEHELO:SetOffsetX(distance) + self.offsetX=distance or 200 + return self +end + +--- Set longitudal offset to carrier. +-- @param #RESCUEHELO self +-- @param #number distance Longitual offset distance in meters. Default 200 m. +-- @return #RESCUEHELO self +function RESCUEHELO:SetOffsetZ(distance) + self.offsetZ=distance or 200 + return self +end + --- Check if tanker is returning to base. -- @param #RESCUEHELO self @@ -235,13 +272,6 @@ function RESCUEHELO:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Land) --self:HandleEvent(EVENTS.Crash) - -- Offset [meters] in the direction of travelling. Positive values are in front of Mother. - local OffsetX=200 - -- Offset [meters] perpendicular to travelling. Positive = Starboard (right of Mother), negative = Port (left of Mother). - local OffsetZ=200 - -- Offset altitude. Should (obviously) always be positve. - local OffsetY=70 - -- Delay before formation is started. local delay=120 @@ -258,7 +288,7 @@ function RESCUEHELO:onafterStart(From, Event, To) local dist=UTILS.NMToMeters(0.2) -- Coordinate behind the carrier - local Carrier=self.carrier:GetCoordinate():SetAltitude(OffsetY):Translate(dist, hdg) + local Carrier=self.carrier:GetCoordinate():SetAltitude(math.min(100, self.altitude)):Translate(dist, hdg) -- Orientation of spawned group. Spawn:InitHeading(hdg) @@ -295,7 +325,7 @@ function RESCUEHELO:onafterStart(From, Event, To) self.formation=AI_FORMATION:New(self.carrier, self.followset, "Helo Formation with Carrier", "Follow Carrier at given parameters.") -- Formation parameters. - self.formation:FormationCenterWing(-OffsetX, 50, math.abs(OffsetY), 50, OffsetZ, 50) + self.formation:FormationCenterWing(-self.offsetX, 50, math.abs(self.altitude), 50, self.offsetZ, 50) -- Start formation FSM. self.formation:__Start(delay) From 2feb293c129ce74e8181e63cd20c2d608ff9c151 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 18 Nov 2018 23:57:41 +0100 Subject: [PATCH 047/147] AIRBOSS v0.2.9 - Added holding check (WIP) - Many improvents and fixes. --- Moose Development/Moose/Core/Radio.lua | 18 + Moose Development/Moose/Ops/Airboss.lua | 330 +++++++++++------- .../Moose/Ops/RecoveryTanker.lua | 20 +- Moose Development/Moose/Utilities/Utils.lua | 14 + 4 files changed, 247 insertions(+), 135 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 4b7908353..1966dad47 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -84,6 +84,7 @@ -- @field #number SubtitleDuration Duration of the Subtitle in seconds. -- @field #number Power Power of the antenna is Watts. -- @field #boolean Loop Transmission is repeated (default true). +-- @field #string alias Name of the radio transmitter. -- @extends Core.Base#BASE RADIO = { ClassName = "RADIO", @@ -94,6 +95,7 @@ RADIO = { SubtitleDuration = 0, Power = 100, Loop = false, + alias=nil, } --- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast. @@ -116,6 +118,22 @@ function RADIO:New(Positionable) return nil end +--- Set alias of the transmitter. +-- @param #RADIO self +-- @param #string alias Name of the radio transmitter. +-- @return #RADIO self +function RADIO:SetAlias(alias) + self.alias=tostring(alias) + return self +end + +--- Get alias of the transmitter. +-- @param #RADIO self +-- @return #string Name of the transmitter. +function RADIO:GetAlias() + return tostring(self.alias) +end + --- Set the file name for the radio transmission. -- @param #RADIO self -- @param #string FileName File name of the sound file (i.e. "Noise.ogg") diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 1155d6813..f33c9bc13 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -4,16 +4,16 @@ -- -- Features: -- --- * CASE I and III recovery. --- * Supports human and AI pilots. +-- * CASE I, II and III recoveries. +-- * Supports human pilots as well as AI. -- * Automatic LSO grading. --- * Different skill level supporting tipps during for students or complete zip lip for pros. +-- * Different skill levels from tipps on-the-fly for students to complete ziplip for pros. -- * Rescue helo option. -- * Recovery tanker option. -- * Voice overs for LSO and AIRBOSS calls. Can easily customized by users. -- * Automatic TACAN and ICLS channel setting. -- * Different radio channels for LSO and airboss calls. --- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels, pilot grades). +-- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels, LSO grades). -- * Multiple carriers supported. -- -- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage. @@ -47,7 +47,6 @@ -- @field #AIRBOSS.RadioCalls radiocall LSO and Airboss call sound files and texts. -- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. --- @field Core.Zone#ZONE_UNIT zoneHolding Zone where aircraft are holding before entering the landing pattern. -- @field Core.Zone#ZONE_UNIT zoneInitial Zone usually 3 NM astern of carrier where pilots start their CASE I pattern. -- @field #table players Table of players. -- @field #table menuadded Table of units where the F10 radio menu was added. @@ -103,7 +102,6 @@ AIRBOSS = { radiocall = {}, zoneCCA = nil, zoneCCZ = nil, - zoneHolding = nil, zoneInitial = nil, players = {}, menuadded = {}, @@ -152,6 +150,7 @@ AIRBOSS.AircraftCarrier={ E2D="E-2C", FA18C="F/A-18C", F14A="F-14A", + --TODO: Add A-A4-E-C } @@ -366,7 +365,7 @@ AIRBOSS.GroovePos={ -- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. -- @field #table debrief Debrief analysis of the current step of this pass. -- @field #table grades LSO grades of player passes. --- @field #boolean inbigzone If true, player is in the big zone. +-- @field #boolean holding If true, player is in holding zone. -- @field #boolean landed If true, player landed or attempted to land. -- @field #boolean bolter If true, LSO told player to bolter. -- @field #boolean boltered If true, player boltered. @@ -375,6 +374,7 @@ AIRBOSS.GroovePos={ -- @field #boolean lig If true, player was long in the groove. -- @field #number Tlso Last time the LSO gave an advice. -- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. +-- @field #table menu F10 radio menu --- Checkpoint parameters triggering the next step in the pattern. -- @type AIRBOSS.Checkpoint @@ -417,12 +417,13 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.2.8" +AIRBOSS.version="0.2.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Get an _OK_ pass if long in groove. Possible other pattern wave offs as well?! -- TODO: Add radio transmission queue for LSO and airboss. -- TODO: Get correct wire when trapped. -- TODO: Add radio check (LSO, AIRBOSS) to F10 radio menu. @@ -489,10 +490,12 @@ function AIRBOSS:New(carriername, alias) -- Set up Airboss radio. self.Carrierradio=RADIO:New(self.carrier) + self.Carrierradio:SetAlias("AIRBOSS") self:SetCarrierradio() -- Set up LSO radio. self.LSOradio=RADIO:New(self.carrier) + self.LSOradio:SetAlias("LSO") self:SetLSOradio() -- Init carrier parameters. @@ -752,7 +755,7 @@ end function AIRBOSS: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.", AIRBOSS.version, self.carrier:GetName(), self.carriertype)) + self:I(self.lid..string.format("Starting AIRBOSS v%s for carrier unit %s of type %s.", AIRBOSS.version, self.carrier:GetName(), self.carriertype)) local theatre=env.mission.theatre @@ -772,6 +775,7 @@ function AIRBOSS:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Land) self:HandleEvent(EVENTS.Crash) + --self:HandleEvent(EVENTS.Ejection) -- Time stamp for checking queues. self.Tqueue=timer.getTime() @@ -818,6 +822,7 @@ function AIRBOSS:onafterStatus(From, Event, To) self:_CheckPlayerStatus() -- Call status every 0.5 seconds. + -- TODO: make dt user input. self:__Status(-0.5) end @@ -1179,7 +1184,6 @@ function AIRBOSS:_PrintQueue(queue, name) for i,_flight in pairs(queue) do local flight=_flight --#AIRBOSS.Flightitem local clock=UTILS.SecondsToClock(flight.time) - -- TODO: add stack alt from flag local stack=flight.flag:Get() local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) local fuel=flight.group:GetFuelMin()*100 @@ -1190,19 +1194,6 @@ function AIRBOSS:_PrintQueue(queue, name) self:I(self.lid..text) end ---- Get carrier coaltion. --- @param #AIRBOSS self -function AIRBOSS:GetCoalition() - return self.carrier:GetCoalition() -end - ---- Get carrier coordinate. --- @param #AIRBOSS self -function AIRBOSS:GetCoordinate() - return self.carrier:GetCoordinate() -end - - --- Scan carrier zone for (new) units. -- @param #AIRBOSS self function AIRBOSS:_ScanCarrierZone() @@ -1312,12 +1303,13 @@ function AIRBOSS:_GetOnboardNumbers(group) -- Onboard number and unit name. local n=tostring(unit.onboard_num) local name=unit.name + local skill=unit.skill -- Table entry. numbers[name]=n -- Debug text. - text=text..string.format("\n- unit=%s - onboard #=%s", name, n) + text=text..string.format("\n- unit %s: onboard #=%s skill=%s", name, n, skill) end -- Debug info. @@ -1399,12 +1391,15 @@ function AIRBOSS:_MarshalPlayer(group) -- Get player data. local playerData=self:_GetPlayerDataGroup(group) + -- Get flight data. local knownflight=self:_GetFlightFromGroupInQueue(group, self.flights) -- Check if flight is known to the airboss already. if playerData and knownflight then -- Add group to marshal stack. self:_AddMarshallGroup(knownflight, nstacks+1) + -- Set step to holding. + playerData.step=AIRBOSS.PatternStep.HOLDING else -- Flight is not registered yet. local text="You are not yet registered inside the CCA. Marshal request denied!" @@ -1485,11 +1480,12 @@ end -- @param #number stack Assigned stack number. Counting starts at one, i.e. stack=1 is the first stack. -- @return #number Holding altitude in meters. -- @return Core.Point#COORDINATE Holding position coordinate. --- @return Core.Point#COORDINATE Second holding position coordinate of racetrack pattern for CASE III recoveries. +-- @return Core.Point#COORDINATE Second holding position coordinate of racetrack pattern for CASE II/III recoveries. function AIRBOSS:_GetMarshalAltitude(stack) -- Carrier position. local Carrier=self:GetCoordinate() + local hdg=self.carrier:GetHeading() -- Altitude of first stack. Depends on recovery case. local angels0 @@ -1501,13 +1497,13 @@ function AIRBOSS:_GetMarshalAltitude(stack) -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next stack. angels0=2 Dist=UTILS.NMToMeters(2.5) - p1=Carrier:Translate(Dist, 280) + p1=Carrier:Translate(Dist, hdg-70) else -- CASE III: Holding at 6000 ft on a racetrack pattern astern the carrier. angels0=6 Dist=UTILS.NMToMeters((stack-1)*angels0+15) - p1=Carrier:Translate(Dist, self:_Radial()) - p2=Carrier:Translate(Dist+UTILS.NMToMeters(10), self:_Radial()) + p1=Carrier:Translate(-Dist, hdg) + p2=Carrier:Translate(-(Dist+UTILS.NMToMeters(10)), hdg) end -- Pattern altitude. @@ -1526,11 +1522,12 @@ function AIRBOSS:_AddMarshallGroup(flight, flagvalue) flight.flag:Set(flagvalue) -- Pressure. - local hPa2inHg=0.0295299830714 - local P=self:GetCoordinate():GetPressure()*hPa2inHg + local P=UTILS.hPa2inHg(self:GetCoordinate():GetPressure()) + + local unitname=flight.group:GetUnit(1):GetName() -- TODO: Get correct board number if possible? - local boardnumber=flight.groupname + local boardnumber=tostring(flight.onboardnumbers[unitname]) local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(flagvalue)) local brc=self:_BaseRecoveryCourse() @@ -1547,7 +1544,7 @@ end -- @param #AIRBOSS self function AIRBOSS:_CollapseMarshalStack() - -- Decrease flag values of all groups in marshal stack. + -- Decrease flag values of all flight groups in marshal stack. for _,_flight in pairs(self.Qmarshal) do local flight=_flight --#AIRBOSS.Flightitem local flagvalue=flight.flag:Get() @@ -1557,6 +1554,7 @@ function AIRBOSS:_CollapseMarshalStack() -- Number of marshal flight groups. local nmarshal=#self.Qmarshal + -- TODO: collapse marschal stack only from N to N-x. For example, when a group in the stack leaves (e.g. for refuelling). for i=nmarshal,1,-1 do local flight=self.Qmarshal[i] --#AIRBOSS.Flightitem --flight. @@ -1565,7 +1563,7 @@ function AIRBOSS:_CollapseMarshalStack() -- First flight to enter the landing pattern. local flight=self.Qmarshal[1] --#AIRBOSS.Flightitem - self:I(self..lid..string.format("New pattern flight %s.", flight.groupname)) + self:I(self.lid..string.format("New pattern flight %s.", flight.groupname)) -- TODO: better message. MESSAGE:New(string.format("Marshal, %s, you are cleared for Case I recovery pattern!", flight.groupname), 15):ToAll() @@ -1573,6 +1571,8 @@ function AIRBOSS:_CollapseMarshalStack() -- Set player step. if flight.ai==false then local playerData=self:_GetPlayerDataGroup(flight.group) + + playerData.step=AIRBOSS.PatternStep.COMMENCING end @@ -1682,26 +1682,13 @@ function AIRBOSS:_CheckPlayerStatus() -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). if unit:IsInZone(self.zoneCCA) then - - -- Check if player was previously not inside the zone. - if playerData.inbigzone==false then - - -- Welcome player once he enters the carrier zone. - local text=string.format("Welcome back, %s! TCN 74X, ICLS 1, BRC 354 (MAG HDG).\n", playerData.callsign) - - -- Heading and distance to register for approach. - local heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) - local distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitialzone:GetCoordinate()) - - -- Send message. - text=text..string.format("Fly heading %d for %.1f NM and turn to BRC.", heading, distance) - MESSAGE:New(text, 5):ToClient(playerData.client) - - end if playerData.step==AIRBOSS.PatternStep.UNDEFINED then - - self:I("Player status undefined. Waiting for next step.") + + -- Status undefined. + local time=timer.getAbsTime() + local clock=UTILS.SecondsToClock(time) + self:I(string.format("Player status undefined. Waiting for next step. Time %s", clock)) -- Jump directly to CASE I straight in approach. --playerData.step=AIRBOSS.PatternStep.COMMENCING @@ -1711,29 +1698,30 @@ function AIRBOSS:_CheckPlayerStatus() playerData.step=AIRBOSS.PatternStep.FINAL self.groovedebug=false end - - elseif playerData.step==AIRBOSS.PatternStep.COMMENCING and unit:InAir() then - - -- New approach. - self:_Commencing(playerData) - + elseif playerData.step==AIRBOSS.PatternStep.HOLDING then - -- TODO: holding check. + -- CASE I/II/III: In holding pattern. + self:_Holding(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.COMMENCING then + + -- CASE I/II/III: New approach. + self:_Commencing(playerData) elseif playerData.step==AIRBOSS.PatternStep.DESCENT4K then - -- CASE III: Initial descent with 4000 ft/min. + -- CASE II/III: Initial descent with 4000 ft/min. self:_Descent4k(playerData) elseif playerData.step==AIRBOSS.PatternStep.DESCENT2K then - -- CASE III: Player has reached 5k "Platform". + -- CASE II/III: Player has reached 5k "Platform". self:_Descent2k(playerData) elseif playerData.step==AIRBOSS.PatternStep.DIRTYUP then - -- CASE III: Player has descended to 1200 ft and is going level from now on. + -- CASE II/III: Player has descended to 1200 ft and is going level from now on. self:_DirtyUp(playerData) elseif playerData.step==AIRBOSS.PatternStep.BULLSEYE then @@ -1743,32 +1731,32 @@ function AIRBOSS:_CheckPlayerStatus() elseif playerData.step==AIRBOSS.PatternStep.INITIAL then - -- Player is at the initial position entering the landing pattern. + -- CASE I/II: Player is at the initial position entering the landing pattern. self:_Initial(playerData) elseif playerData.step==AIRBOSS.PatternStep.UPWIND then - -- Upwind leg aka break entry. + -- CASE I/II: Upwind leg aka break entry. self:_Upwind(playerData) elseif playerData.step==AIRBOSS.PatternStep.EARLYBREAK then - -- Early break. + -- CASE I/II: Early break. self:_Break(playerData, "early") elseif playerData.step==AIRBOSS.PatternStep.LATEBREAK then - -- Late break. + -- CASE I/II: Late break. self:_Break(playerData, "late") elseif playerData.step==AIRBOSS.PatternStep.ABEAM then - -- Abeam position. + -- CASE I/II: Abeam position. self:_Abeam(playerData) elseif playerData.step==AIRBOSS.PatternStep.NINETY then - -- Check long down wind leg. + -- CASE:I/II: Check long down wind leg. self:_CheckForLongDownwind(playerData) -- At the ninety. @@ -1776,12 +1764,12 @@ function AIRBOSS:_CheckPlayerStatus() elseif playerData.step==AIRBOSS.PatternStep.WAKE then - -- In the wake. + -- CASE I/II: In the wake. self:_Wake(playerData) elseif playerData.step==AIRBOSS.PatternStep.FINAL then - -- Turn to final and enter the groove. + -- CASE I/II: Turn to final and enter the groove. self:_Final(playerData) elseif playerData.step==AIRBOSS.PatternStep.GROOVE_XX or @@ -1791,7 +1779,7 @@ function AIRBOSS:_CheckPlayerStatus() playerData.step==AIRBOSS.PatternStep.GROOVE_AR or playerData.step==AIRBOSS.PatternStep.GROOVE_IW then - -- In the groove. + -- CASE I/II: In the groove. self:_Groove(playerData) elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then @@ -1805,7 +1793,8 @@ function AIRBOSS:_CheckPlayerStatus() end else - playerData.inbigzone=false + --playerData.inbigzone=false + self:E(self.lid.."WARNING: Player left the CCA!") end else @@ -1925,6 +1914,9 @@ function AIRBOSS:OnEventLand(EventData) local lp=coord:MarkToAll("Landing coord.") coord:SmokeGreen() + -- Landing distance to carrier position. + local dist=coord:Get2DDistance(self:GetCoordinate()) + -- Debug marks of wires. local w1=self:GetCoordinate():Translate(self.carrierparam.wire1, 0):MarkToAll("Wire 1") local w2=self:GetCoordinate():Translate(self.carrierparam.wire2, 0):MarkToAll("Wire 2") @@ -1938,13 +1930,24 @@ function AIRBOSS:OnEventLand(EventData) playerData.step=AIRBOSS.PatternStep.UNDEFINED -- Call trapped function in 3 seconds to make sure we did not bolter. - SCHEDULER:New(nil, self._Trapped,{self, playerData, coord}, 3) + SCHEDULER:New(nil, self._Trapped,{self, playerData, dist}, 3) end else -- TODO: Get landing coodinates of AI hornet for perfect _OK_ 3-wire pass! + -- Coordinate at landing event + local coord=EventData.IniUnit:GetCoordinate() + + -- Debug mark of player landing coord. + local dist=coord:Get2DDistance(self:GetCoordinate()) + + local text=string.format("AI landing dist=%.1f m", dist) + env.info(text) + + local lp=coord:MarkToAll(text) + coord:SmokeGreen() -- AI: Decrease number of units in flight and remove group from pattern queue if all units landed. if self:_InQueue(self.Qpattern, EventData.IniGroup) then @@ -2018,7 +2021,7 @@ function AIRBOSS:_NewPlayer(unitname) playerData.difficulty=playerData.difficulty or AIRBOSS.Difficulty.NORMAL -- Player is in the big zone around the carrier. - playerData.inbigzone=playerData.unit:IsInZone(self.zoneCCA) + --playerData.inbigzone=playerData.unit:IsInZone(self.zoneCCA) -- Init stuff for this round. playerData=self:_InitPlayer(playerData) @@ -2047,17 +2050,92 @@ function AIRBOSS:_InitPlayer(playerData) playerData.bolter=false playerData.boltered=false playerData.landed=false + playerData.holding=nil playerData.Tlso=timer.getTime() return playerData end +--- Holding. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Holding(playerData) + + local unit=playerData.unit + local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) + local stack=flight.flag:Get() + + local alt, c1, c2=self:_GetMarshalAltitude(stack) + + -- Create a holding zone depending on recovery case. + local zoneHolding --Core.Zone#ZONE + if self.case==1 then + -- CASE I + + -- Zone 2.5 NM port of carrier with a radius of 3 NM (holding pattern should be < 5 NM). + zoneHolding=ZONE_UNIT:New("CASE I Holding Zone", self.carrier, UTILS.NMToMeters(3), {dx=0, dy=-UTILS.NMToMeters(2.5), relative_to_unit=true}) + + else + -- CASE II/II + + local hdg=self.carrier:GetHeading() + + -- Create an array of a square! + local p={} + p[1]=c1:GetVec2() + p[2]=c2:GetVec2() + p[3]=c2:Translate(UTILS.NMToMeters(5), hdg-90):GetVec2() + p[4]=c1:Translate(UTILS.NMToMeters(5), hdg-90):GetVec2() + + zoneHolding=ZONE_POLYGON_BASE:New("CASE II/III Holding Zone", p) + end + + + --if bla then + -- zoneHolding:SmokeZone(SMOKECOLOR.Green) + -- bla=false + --end + + -- Check if player is in holding zone. + local inholdingzone=unit:IsInZone(zoneHolding) + + -- Different cases + if playerData.holding==true then + -- Player was in holding zone last time we checked. + if inholdingzone then + -- Player is still in holding zone. + self:I("Player is still in the holding zone. Good job.") + else + -- Player left the holding zone. + self:I("Player just left the holding zone. Come back!") + end + elseif playerData.holding==false then + -- Player left holding zone + if inholdingzone then + -- Player is back in the holding zone. + self:I("Player is back in the holding zone after leaving it.") + else + -- Player is still outside the holding zone. + self:I("Player still outside the holding zone. What are you doing man?!") + end + elseif playerData.holding==nil then + -- Player never entered the holding zone + if inholdingzone then + -- Player arrived in holding zone. + playerData.holding=true + self:I("Player entered the holding zone for the first time.") + else + -- Player did not yet arrive in holding zone. + self:I("Waiting for player to arrive in the holding zone.") + end + end + +end + --- Commence approach. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. function AIRBOSS:_Commencing(playerData) - - local text="Commencing." -- Initialize player data for new approach. self:_InitPlayer(playerData) @@ -2071,8 +2149,11 @@ function AIRBOSS:_Commencing(playerData) playerData.step=AIRBOSS.PatternStep.DESCENT4K end + + local text="Commencing." + -- Message to player. - self:_SendMessageToPlayer(text, 10, playerData) + self:_SendMessageToPlayer(text, 10, playerData) end --- Start pattern when player enters the initial zone. @@ -2600,10 +2681,6 @@ function AIRBOSS:_Groove(playerData) if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX then -- LSO "Call the ball" call. - -- TODO: take this out. - --self:_SendMessageToPlayer("Call the ball.", 5, playerData) - - -- LSO radio call. self:RadioTransmission(self.LSOradio, self.radiocall.CALLTHEBALL) playerData.Tlso=timer.getTime() @@ -2616,10 +2693,6 @@ function AIRBOSS:_Groove(playerData) elseif rho<=RRB and playerData.step==AIRBOSS.PatternStep.GROOVE_RB then -- Pilot: "Roger ball" call. - -- TODO: take this out. - --self:_SendMessageToPlayer("Roger ball!", 5, playerData) - - -- LSO radio call. self:RadioTransmission(self.LSOradio, self.radiocall.ROGERBALL) playerData.Tlso=timer.getTime()+1 @@ -2659,10 +2732,7 @@ function AIRBOSS:_Groove(playerData) -- Let's see.. if waveoff then - -- Wave off player. - --self:_SendMessageToPlayer("Wave off!", 10, playerData) - - -- LSO radio call. + -- LSO Wave off! self:RadioTransmission(self.LSOradio, self.radiocall.WAVEOFF) playerData.Tlso=timer.getTime() @@ -2751,8 +2821,9 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA) end -- Too slow or too fast? + -- TODO: Only apply for TOPGUN graduate skill level or at least not for Flight Student level. if AoA<6.9 or AoA>9.3 then - self:I(self.lid.."DEACTIVE! Wave off due to AoA<6.9 or AoA>9.3!") + self:I(self.lid.."INACTIVE! Wave off due to AoA<6.9 or AoA>9.3!") --waveoff=true end @@ -2764,19 +2835,19 @@ end --- Trapped? -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. --- @param Core.Point#COORDINATE pos Position of aircraft on landing event. -function AIRBOSS:_Trapped(playerData, pos) +-- @param #number X Distance in meters wrt carrier position where player landed. +function AIRBOSS:_Trapped(playerData, X) self:I(self.lid.."FF TRAPPED") -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi = self:_GetDistances(pos) + --local X, Z, rho, phi = self:_GetDistances(pos) if playerData.unit:InAir()==false then -- Seems we have successfully landed. -- Little offset for the exact wire positions. - local wdx=11 + local wdx=0 -- Which wire was caught? local wire @@ -2864,7 +2935,7 @@ function AIRBOSS:_DetailedPlayerStatus(playerData) text=text..string.format("\nGS Error = %.1f° (glide slope)", glideslope) end - -- Wind + -- Wind (for debugging). --text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) @@ -3761,7 +3832,7 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay) end end ---- Send message to playe client. +--- Send message to player client. -- @param #AIRBOSS self -- @param #string message The message to send. -- @param #number duration Display message duration. @@ -3771,21 +3842,18 @@ end -- @param #number delay Delay in seconds, before the message is send. function AIRBOSS:_SendMessageToPlayer(message, duration, playerData, clear, sender, delay) - if message then - - delay=delay or 0 - sender=sender or self.alias - - local text=string.format("%s, %s, %s", sender, playerData.callsign, message) + if playerData and message then + + -- Format message. + local text=string.format("%s, %s", playerData.callsign, message) self:I(self.lid..text) - if delay>0 then - SCHEDULER:New(nil,self._SendMessageToPlayer, {self, message, duration, playerData, clear, sender}, delay) + if delay and delay>0 then + SCHEDULER:New(nil, self._SendMessageToPlayer, {self, message, duration, playerData, clear, sender}, delay) else if playerData.client then - MESSAGE:New(text, duration, nil, clear):ToClient(playerData.client) - end - --MESSAGE:New(text, duration, nil, clear):ToAll() + MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) + end end end @@ -3888,6 +3956,20 @@ function AIRBOSS:_GetPlayerUnitAndName(_unitName) return nil,nil end +--- Get carrier coaltion. +-- @param #AIRBOSS self +-- @return #number Coalition side of carrier. +function AIRBOSS:GetCoalition() + return self.carrier:GetCoalition() +end + +--- Get carrier coordinate. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Carrier coordinate. +function AIRBOSS:GetCoordinate() + return self.carrier:GetCoordinate() +end + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Menu Functions ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3913,10 +3995,10 @@ function AIRBOSS:_AddF10Commands(_unitName) if not self.menuadded[_gid] then -- Enable switch so we don't do this twice. - self.menuadded[_gid] = true + self.menuadded[_gid]=true -- Main F10 menu: F10/Airboss// - if AIRBOSS.MenuF10[_gid] == nil then + if AIRBOSS.MenuF10[_gid]==nil then AIRBOSS.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "Airboss") end @@ -3933,7 +4015,7 @@ function AIRBOSS:_AddF10Commands(_unitName) local _skillPath = missionCommands.addSubMenuForGroup(_gid, "Skill Level", _rootPath) -- F10/Airboss//My Settings/Kneeboard - --local _kneeboardPath = missionCommands.addSubMenuForGroup(_gid, "Kneeboard", _rootPath) + local _kneeboardPath = missionCommands.addSubMenuForGroup(_gid, "Kneeboard", _rootPath) -- F10/Airboss//LSO Grades/ missionCommands.addCommandForGroup(_gid, "Greenie Board", _statsPath, self._DisplayScoreBoard, self, _unitName) @@ -3944,18 +4026,18 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) missionCommands.addCommandForGroup(_gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) - missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _skillPath, self._AttitudeMonitor, self, playername) + + -- F10/Airboss//Kneeboard + missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _kneeboardPath, self._AttitudeMonitor, self, playername) + missionCommands.addCommandForGroup(_gid, "Weather Report", _kneeboardPath, self._DisplayCarrierWeather, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Carrier Info", _kneeboardPath, self._DisplayCarrierInfo, self, _unitName) + -- F10/Airboss// - missionCommands.addCommandForGroup(_gid, "Weather Report", _rootPath, self._DisplayCarrierWeather, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Carrier Info", _rootPath, self._DisplayCarrierInfo, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Request Straight-In", _rootPath, self._RequestStraightIn, self, _unitName) - - -- TODO: request straight in approach - -- TODO: request refuelling. - -- - + missionCommands.addCommandForGroup(_gid, "Request Marshal?", _rootPath, self._RequestMarshal, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Commencing!", _rootPath, self._RequestStraightIn, self, _unitName) + + --TODO: request refulling if recovery tanker set! make refuelling queue. add refuelling step. end else @@ -3981,7 +4063,7 @@ function AIRBOSS:_RequestStraightIn(_unitName) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then - self:_MarshalPlayer(_unit:GetGroup()) + playerData.step=AIRBOSS.PatternStep.COMMENCING end end end @@ -4224,17 +4306,15 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) 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) + local tP=string.format("%.1f mmHg", UTILS.hPa2mmHg(P)) 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) + tP=string.format("%.2f inHg", UTILS.hPa2inHg(P)) end -- Report text. diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index f63e75d17..80f73f555 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -15,7 +15,7 @@ -- -- ### Author: **funkyfranky** -- --- @module Ops.CarrierTanker +-- @module Ops.RecoveryTanker -- @image MOOSE.JPG --- RECOVERYTANKER class. @@ -36,13 +36,13 @@ -- @field #number lowfuel Low fuel threshold in percent. -- @extends Core.Fsm#FSM ---- Carrier Tanker. +--- Recovery Tanker. -- -- === -- -- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Main.jpg) -- --- # Carrier Tanker +-- # Recovery Tanker -- -- bla bla -- @@ -129,11 +129,11 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:AddTransition("Running", "Stop", "Stopped") - --- Triggers the FSM event "Start" that starts the carrier tanker. Initializes parameters and starts event handlers. + --- Triggers the FSM event "Start" that starts the recovery tanker. Initializes parameters and starts event handlers. -- @function [parent=#RECOVERYTANKER] Start -- @param #RECOVERYTANKER self - --- Triggers the FSM event "Start" that starts the carrier tanker after a delay. Initializes parameters and starts event handlers. + --- Triggers the FSM event "Start" that starts the recovery tanker after a delay. Initializes parameters and starts event handlers. -- @function [parent=#RECOVERYTANKER] __Start -- @param #RECOVERYTANKER self -- @param #number delay Delay in seconds. @@ -147,11 +147,11 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- @param #RECOVERYTANKER self -- @param #number delay Delay in seconds. - --- Triggers the FSM event "Stop" that stops the carrier tanker. Event handlers are stopped. + --- Triggers the FSM event "Stop" that stops the recovery tanker. Event handlers are stopped. -- @function [parent=#RECOVERYTANKER] Stop -- @param #RECOVERYTANKER self - --- Triggers the FSM event "Stop" that stops the carrier tanker after a delay. Event handlers are stopped. + --- Triggers the FSM event "Stop" that stops the recovery tanker after a delay. Event handlers are stopped. -- @function [parent=#RECOVERYTANKER] __Stop -- @param #RECOVERYTANKER self -- @param #number delay Delay in seconds. @@ -280,7 +280,7 @@ end function RECOVERYTANKER:onafterStart(From, Event, To) -- Info on start. - self:I(string.format("Starting Carrier Tanker v%s for carrier unit %s of type %s for tanker group %s.", RECOVERYTANKER.version, self.carrier:GetName(), self.carriertype, self.tankergroupname)) + self:I(string.format("Starting Recovery Tanker v%s for carrier unit %s of type %s for tanker group %s.", RECOVERYTANKER.version, self.carrier:GetName(), self.carriertype, self.tankergroupname)) -- Handle events. self:HandleEvent(EVENTS.EngineShutdown) @@ -429,7 +429,7 @@ end -- EVENT functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Event handler for engine shutdown of carrier tanker. +--- Event handler for engine shutdown of recovery tanker. -- Respawn tanker group once it landed because it was out of fuel. -- @param #RECOVERYTANKER self -- @param Core.Event#EVENTDATA EventData Event data. @@ -445,7 +445,7 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) if groupname:match(self.tankergroupname) then -- Debug info. - self:I(string.format("CARIERTANKER: Respawning group %s.", group:GetName())) + self:I(string.format("Respawning recovery tanker group %s.", group:GetName())) -- Respawn tanker. self.tanker=group:RespawnAtCurrentAirbase() diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index c8c61c06d..e9aa475ce 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -294,6 +294,20 @@ UTILS.CelciusToFarenheit = function( Celcius ) return Celcius * 9/5 + 32 end +--- Convert pressure from hecto Pascal (hPa) to inches of mercury (inHg). +-- @param #number hPa Pressure in hPa. +-- @return #number Pressure in inHg. +UTILS.hPa2inHg = function( hPa ) + return hPa * 0.0295299830714 +end + +--- Convert pressure from hecto Pascal (hPa) to millimeters of mercury (mmHg). +-- @param #number hPa Pressure in hPa. +-- @return #number Pressure in mmHg. +UTILS.hPa2mmHg = function( hPa ) + return hPa * 0.7500615613030 +end + --[[acc: From 51a1f5601130a9fcaebad9bede1de75ff95de20b Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 20 Nov 2018 00:15:39 +0100 Subject: [PATCH 048/147] AIRBOSS v0.3.0 RESCUEHELO v0.9.2 RECOVERYTANKER v0.9.2 --- Moose Development/Moose/AI/AI_Formation.lua | 2 +- Moose Development/Moose/Core/Point.lua | 2 +- .../Moose/Functional/Warehouse.lua | 5 +- Moose Development/Moose/Ops/Airboss.lua | 205 ++++++++---- .../Moose/Ops/RecoveryTanker.lua | 293 ++++++++++++++---- Moose Development/Moose/Ops/RescueHelo.lua | 267 ++++++++++++---- 6 files changed, 593 insertions(+), 181 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index c6f597ca8..c02096609 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -906,7 +906,7 @@ function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 end ---- Follow event fuction. Check if coming from state "stopped". If so the transition is rejected. +--- Stop function. Formation will not be updated any more. -- @param #AI_FORMATION self -- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. -- @param #string From From state. diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 8a6e0ffda..b79174989 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1132,7 +1132,7 @@ do -- COORDINATE -- HeliGroup:Route( { LandWaypoint }, 1 ) -- Start landing the helicopter in one second. -- function COORDINATE:WaypointAirLanding( Speed, airbase, DCSTasks, description ) - return self:WaypointAir( nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, airbase, DCSTasks, description ) + return self:WaypointAir(nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, nil, airbase, DCSTasks, description) end diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 55ee5e9d8..556d93afd 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1790,7 +1790,7 @@ WAREHOUSE.version="0.6.6" --- The WAREHOUSE constructor. Creates a new WAREHOUSE object from a static object. Parameters like the coalition and country are taken from the static object structure. -- @param #WAREHOUSE self --- @param Wrapper.Static#STATIC warehouse The physical structure of the warehouse. +-- @param Wrapper.Static#STATIC warehouse The physical structure representing the warehouse. -- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static -- @return #WAREHOUSE self function WAREHOUSE:New(warehouse, alias) @@ -1798,8 +1798,9 @@ function WAREHOUSE:New(warehouse, alias) -- Check if just a string was given and convert to static. if type(warehouse)=="string" then - warehouse=GROUP:FindByName(warehouse) + warehouse=UNIT:FindByName(warehouse) if warehouse==nil then + env.info(string.format("FF no warehouse unit with name %s found trying static.", warehouse)) warehouse=STATIC:FindByName(warehouse, true) end end diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index f33c9bc13..38b52fd46 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -10,11 +10,11 @@ -- * Different skill levels from tipps on-the-fly for students to complete ziplip for pros. -- * Rescue helo option. -- * Recovery tanker option. --- * Voice overs for LSO and AIRBOSS calls. Can easily customized by users. +-- * Voice overs for LSO and AIRBOSS calls. Can easily be customized by users. -- * Automatic TACAN and ICLS channel setting. -- * Different radio channels for LSO and airboss calls. -- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels, LSO grades). --- * Multiple carriers supported. +-- * Multiple carriers supported (due to object oriented approach). -- -- **PLEASE NOTE** 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 John C. Stennis as carrier. @@ -68,6 +68,7 @@ -- @field #table Qpattern Queue of aircraft groups in the landing pattern. -- @field Ops.RescueHelo#RESCUEHELO rescuehelo Rescue helo flying in close formation with the carrier. -- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. +-- @field Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. -- @field #table recoverytime List of time intervals when aircraft are recovered. -- @extends Core.Fsm#FSM @@ -123,6 +124,7 @@ AIRBOSS = { Qmarshal = {}, rescuehelo = nil, tanker = nil, + warehouse = nil, recoverytime = {}, } @@ -137,6 +139,8 @@ AIRBOSS.AircraftPlayer={ --- Aircraft types capable of landing on carrier (human+AI). -- @type AIRBOSS.AircraftCarrier +-- @field #string AV8B AV-8B Night Harrier. +-- @field #string HORNET F/A-18C Lot 20 Hornet. -- @field #string S3B Lockheed S-3B Viking. -- @field #string S3BTANKER Lockheed S-3B Viking tanker. -- @field #string E2D Grumman E-2D Hawkeye AWACS. @@ -150,7 +154,7 @@ AIRBOSS.AircraftCarrier={ E2D="E-2C", FA18C="F/A-18C", F14A="F-14A", - --TODO: Add A-A4-E-C + --TODO: Add A4-E-C } @@ -417,12 +421,13 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.2.9" +AIRBOSS.version="0.3.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Set case II and III times. -- TODO: Get an _OK_ pass if long in groove. Possible other pattern wave offs as well?! -- TODO: Add radio transmission queue for LSO and airboss. -- TODO: Get correct wire when trapped. @@ -459,7 +464,10 @@ AIRBOSS.version="0.2.9" function AIRBOSS:New(carriername, alias) -- Inherit everthing from FSM class. - local self = BASE:Inherit(self, FSM:New()) -- #AIRBOSS + local self=BASE:Inherit(self, FSM:New()) -- #AIRBOSS + + -- Debug. + self:F2({carriername=carriername, alias=alias}) -- Set carrier unit. self.carrier=UNIT:FindByName(carriername) @@ -515,8 +523,8 @@ function AIRBOSS:New(carriername, alias) return nil end - -- Zone 3 NM astern and 100 m starboard of the carrier with radius of 2.0 km. - self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, 2.0*1000, {dx=-UTILS.NMToMeters(3), dy=100, relative_to_unit=true}) + -- Zone 3 NM astern and 100 m starboard of the carrier with radius of 0.5 km. + self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, 0.5*1000, {dx=-UTILS.NMToMeters(3), dy=100, relative_to_unit=true}) -- CCA 50 NM radius zone around the carrier. self:SetCarrierControlledArea() @@ -729,6 +737,35 @@ function AIRBOSS:SetCarrierradio(frequency, modulation) end +--- Define rescue helicopter associated with the carrier. +-- @param #AIRBOSS self +-- @param Ops.RescueHelo#RESCUEHELO rescuehelo Rescue helo object. +-- @return #ARIBOSS self +function AIRBOSS:SetRescueHelo(rescuehelo) + self.rescuehelo=rescuehelo + return self +end + +--- Define recovery tanker associated with the carrier. +-- @param #AIRBOSS self +-- @param Ops.RecoveryTanker#RECOVERYTANKER recoverytanker Recovery tanker object. +-- @return #ARIBOSS self +function AIRBOSS:SetRecoveryTanker(recoverytanker) + self.tanker=recoverytanker + return self +end + + +--- Define warehouse associated with the carrier. +-- @param #AIRBOSS self +-- @param Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. +-- @return #ARIBOSS self +function AIRBOSS:SetWarehouse(warehouse) + self.warehouse=warehouse + return self +end + + --- Check if carrier is recovering aircraft. -- @param #AIRBOSS self -- @return #boolean If true, time slot for recovery is open. @@ -805,8 +842,8 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Update marshal and pattern queue every 30 seconds. if time-self.Tqueue>30 then - local text=string.format("AIRBOSS %s: Status %s.", self.alias, self:GetState()) - self:I(text) + local text=string.format("Status %s.", self:GetState()) + self:I(self.lid..text) -- Scan carrier zone for new aircraft. self:_ScanCarrierZone() @@ -946,14 +983,15 @@ function AIRBOSS:_InitStennis() -- 4k descent from holding pattern to 5k platform self.C3Descent4k.name="4k Descent" - self.C3Descent4k.Xmin=-UTILS.NMToMeters(35) + self.C3Descent4k.Xmin=-UTILS.NMToMeters(50) self.C3Descent4k.Xmax=-UTILS.NMToMeters(20) - self.C3Descent4k.Zmin=-UTILS.NMToMeters(30) - self.C3Descent4k.Zmax= UTILS.NMToMeters(30) + self.C3Descent4k.Zmin=-UTILS.NMToMeters(10) + self.C3Descent4k.Zmax= UTILS.NMToMeters(3) self.C3Descent4k.LimitXmin=nil self.C3Descent4k.LimitXmax=-UTILS.NMToMeters(20) --TODO: better rho dist. decrease descent 20 2000 ft/min at 5000 ft alt and user rad alt. self.C3Descent4k.LimitZmin=nil self.C3Descent4k.LimitZmax=nil + -- TODO: alt, AoA are more aircraft functions rather than carrier self.C3Descent4k.Altitude=nil --UTILS.FeetToMeters(5000) self.C3Descent4k.AoA=nil self.C3Descent4k.Distance=nil @@ -1004,7 +1042,7 @@ function AIRBOSS:_InitStennis() self.Upwind.name="Upwind" self.Upwind.Xmin=-UTILS.NMToMeters(4) self.Upwind.Xmax=nil - self.Upwind.Zmin=0 + self.Upwind.Zmin=-100 self.Upwind.Zmax=1000 self.Upwind.LimitXmin=0 self.Upwind.LimitXmax=nil @@ -1018,8 +1056,8 @@ function AIRBOSS:_InitStennis() self.BreakEarly.name="Early Break" self.BreakEarly.Xmin=-500 self.BreakEarly.Xmax=UTILS.NMToMeters(5) - self.BreakEarly.Zmin=-3700 - self.BreakEarly.Zmax=1500 + self.BreakEarly.Zmin=-UTILS.NMToMeters(2) + self.BreakEarly.Zmax=UTILS.NMToMeters(1) self.BreakEarly.LimitXmin=0 self.BreakEarly.LimitXmax=nil self.BreakEarly.LimitZmin=-370 -- 0.2 NM port of carrier @@ -1032,8 +1070,8 @@ function AIRBOSS:_InitStennis() self.BreakLate.name="Late Break" self.BreakLate.Xmin=-500 self.BreakLate.Xmax=UTILS.NMToMeters(5) - self.BreakLate.Zmin=-3700 - self.BreakLate.Zmax=1500 + self.BreakLate.Zmin=-UTILS.NMToMeters(2) + self.BreakLate.Zmax=UTILS.NMToMeters(1) self.BreakLate.LimitXmin=0 self.BreakLate.LimitXmax=nil self.BreakLate.LimitZmin=-1470 --0.8 NM @@ -1156,6 +1194,8 @@ function AIRBOSS:_CheckQueue() local TpatternMin=120 if self.case==1 then TpatternMin=45 + else + TpatternMin=120 end -- Min time in marshal before send to landing pattern. @@ -1416,6 +1456,12 @@ function AIRBOSS:_MarshalAI(flight) -- Flight group name. local group=flight.group local groupname=flight.groupname + + -- Check that we do not add a recovery tanker for marshaling. + -- TODO: Fix group name. + if self.tanker and self.tanker.tanker:GetName()==groupname then + return + end -- Number of already full marshal stacks. local nstacks=#self.Qmarshal @@ -1499,7 +1545,7 @@ function AIRBOSS:_GetMarshalAltitude(stack) Dist=UTILS.NMToMeters(2.5) p1=Carrier:Translate(Dist, hdg-70) else - -- CASE III: Holding at 6000 ft on a racetrack pattern astern the carrier. + -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. angels0=6 Dist=UTILS.NMToMeters((stack-1)*angels0+15) p1=Carrier:Translate(-Dist, hdg) @@ -1688,10 +1734,8 @@ function AIRBOSS:_CheckPlayerStatus() -- Status undefined. local time=timer.getAbsTime() local clock=UTILS.SecondsToClock(time) - self:I(string.format("Player status undefined. Waiting for next step. Time %s", clock)) + self:T3(string.format("Player status undefined. Waiting for next step. Time %s", clock)) - -- Jump directly to CASE I straight in approach. - --playerData.step=AIRBOSS.PatternStep.COMMENCING -- Jump to final/groove for testing. if self.groovedebug then @@ -1853,7 +1897,7 @@ function AIRBOSS:OnEventBirth(EventData) --self:RadioTransmission(self.LSOradio, self.radiocall.LONGINGROOVE, false, 20) -- Start in the groove for debugging. - self.groovedebug=false + self.groovedebug=true end end @@ -1887,6 +1931,7 @@ function AIRBOSS:OnEventLand(EventData) self:T3(self.lid.."LAND: player = "..tostring(_playername)) if _unit and _playername then + -- Human Player landed. local _uid=_unit:GetID() local _group=_unit:GetGroup() @@ -1935,19 +1980,19 @@ function AIRBOSS:OnEventLand(EventData) end else - - -- TODO: Get landing coodinates of AI hornet for perfect _OK_ 3-wire pass! - -- Coordinate at landing event - local coord=EventData.IniUnit:GetCoordinate() - - -- Debug mark of player landing coord. - local dist=coord:Get2DDistance(self:GetCoordinate()) - - local text=string.format("AI landing dist=%.1f m", dist) - env.info(text) + -- AI unit landed. + + -- Coordinate at landing event + local coord=EventData.IniUnit:GetCoordinate() + + -- Debug mark of player landing coord. + local dist=coord:Get2DDistance(self:GetCoordinate()) + + local text=string.format("AI landing dist=%.1f m", dist) + env.info(text) - local lp=coord:MarkToAll(text) - coord:SmokeGreen() + local lp=coord:MarkToAll(text) + coord:SmokeGreen() -- AI: Decrease number of units in flight and remove group from pattern queue if all units landed. if self:_InQueue(self.Qpattern, EventData.IniGroup) then @@ -1974,10 +2019,11 @@ function AIRBOSS:OnEventCrash(EventData) -- TODO: Update queues! + -- TODO: decrease number of units in group if _unit and _playername then self:I(self.lid.."Player %s crashed!",_playername) else - self:I(self.lid.."AI unit %s crashed!", EventData.IniUnitName) + self:I(self.lid.."AI unit %s crashed!", EventData.IniUnitName) end end @@ -2056,16 +2102,25 @@ function AIRBOSS:_InitPlayer(playerData) return playerData end +local _bla=true + --- Holding. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. function AIRBOSS:_Holding(playerData) + -- Player unit and flight. local unit=playerData.unit local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) + + -- Current stack. local stack=flight.flag:Get() - local alt, c1, c2=self:_GetMarshalAltitude(stack) + -- Pattern alitude. + local patternalt, c1, c2=self:_GetMarshalAltitude(stack) + + -- Player altitude. + local playeralt=unit:GetAltitude() -- Create a holding zone depending on recovery case. local zoneHolding --Core.Zone#ZONE @@ -2082,52 +2137,87 @@ function AIRBOSS:_Holding(playerData) -- Create an array of a square! local p={} - p[1]=c1:GetVec2() - p[2]=c2:GetVec2() - p[3]=c2:Translate(UTILS.NMToMeters(5), hdg-90):GetVec2() - p[4]=c1:Translate(UTILS.NMToMeters(5), hdg-90):GetVec2() + p[1]=c1:Translate(UTILS.NMToMeters(1), hdg+90):GetVec2() --c1 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. + p[2]=c2:Translate(UTILS.NMToMeters(1), hdg+90):GetVec2() --c2 is 10 NM further behind. Also translated 1 NM starboard. + p[3]=c2:Translate(UTILS.NMToMeters(7), hdg-90):GetVec2() --p3 6 NM port of carrier. + p[4]=c1:Translate(UTILS.NMToMeters(7), hdg-90):GetVec2() --p4 6 NM port of carrier. + -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. + -- So stay 0-5 NM (+1 NM error margin) port of carrier. zoneHolding=ZONE_POLYGON_BASE:New("CASE II/III Holding Zone", p) end - --if bla then - -- zoneHolding:SmokeZone(SMOKECOLOR.Green) - -- bla=false - --end + if _bla then + zoneHolding:SmokeZone(SMOKECOLOR.Green) + _bla=false + end -- Check if player is in holding zone. - local inholdingzone=unit:IsInZone(zoneHolding) + local inholdingzone=unit:IsInZone(zoneHolding) + + -- Check player alt is +-500 feet of assigned pattern alt. + local altdiff=playeralt-patternalt + local goodalt=math.abs(altdiff)90 and self:_CheckLimits(X, Z, self.Wake) then -- Message to player. self:_SendMessageToPlayer("You are already at the wake and have not passed the 90! Turn faster next time!", 10, playerData) + --TODO: pattern WO? end end @@ -4030,9 +4128,9 @@ function AIRBOSS:_AddF10Commands(_unitName) -- F10/Airboss//Kneeboard missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _kneeboardPath, self._AttitudeMonitor, self, playername) - missionCommands.addCommandForGroup(_gid, "Weather Report", _kneeboardPath, self._DisplayCarrierWeather, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Carrier Info", _kneeboardPath, self._DisplayCarrierInfo, self, _unitName) - + missionCommands.addCommandForGroup(_gid, "Weather Report", _kneeboardPath, self._DisplayCarrierWeather, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Carrier Info", _kneeboardPath, self._DisplayCarrierInfo, self, _unitName) + -- F10/Airboss// missionCommands.addCommandForGroup(_gid, "Request Marshal?", _rootPath, self._RequestMarshal, self, _unitName) missionCommands.addCommandForGroup(_gid, "Commencing!", _rootPath, self._RequestStraightIn, self, _unitName) @@ -4063,6 +4161,8 @@ function AIRBOSS:_RequestStraightIn(_unitName) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then + -- TODO: check if landing pattern is full. If so, display message "AIRBOSS: "Pattern is full." and deny step! + -- TODO: check if in marshal stack and flag is 0. If not, give message "AIRBOSS: It's not your turn yet!" and deny step! playerData.step=AIRBOSS.PatternStep.COMMENCING end end @@ -4338,4 +4438,3 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 80f73f555..58518093d 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -5,9 +5,8 @@ -- Features: -- -- * Regular pattern update with respect to carrier positon. --- * Automatic respawning when tanker runs out of fuel. --- * Tanker can be spawned cold or hot on the carrier or any other airbase or directly in air. --- * Tanker can operate 24/7. +-- * Automatic respawning when tanker runs out of fuel for 24/7 operations. +-- * Tanker can be spawned cold or hot on the carrier or at any other airbase or directly in air. -- -- Please not that his class is work in progress and in an **alpha** stage. -- @@ -34,6 +33,9 @@ -- @field #number Tupdate Last time the pattern was updated. -- @field #number takeoff Takeoff type (cold, hot, air). -- @field #number lowfuel Low fuel threshold in percent. +-- @field #boolean respawn If true, tanker be respawned (default). If false, no respawning will happen. +-- @field #boolean respawninair If true, tanker will always be respawned in air. This has no impact on the initial spawn setting. +-- @field #boolean uncontrolledac If true, use and uncontrolled tanker group already present in the mission. -- @extends Core.Fsm#FSM --- Recovery Tanker. @@ -62,21 +64,24 @@ RECOVERYTANKER = { Tupdate = nil, takeoff = nil, lowfuel = nil, + respawn = nil, + respawninair = nil, + uncontrolledac = nil, } --- Class version. -- @field #string version -RECOVERYTANKER.version="0.9.1" +RECOVERYTANKER.version="0.9.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Possibility to add already present/spawned aircraft, e.g. for warehouse. --- TODO: Write documenation. -- TODO: Smarter pattern update function. E.g. (small) zone around carrier. Only update position when carrier leaves zone or changes heading? --- TODO: Maybe rework pattern update implementation altogether to make it smoother. +-- TODO: Write documenation. +-- DONE: Add refueling event/state. +-- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -104,7 +109,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- Tanker group name. self.tankergroupname=tankergroupname - -- Default parameters. + -- Init default parameters. self:SetPatternUpdateInterval() self:SetAltitude() self:SetSpeed() @@ -112,6 +117,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) self:SetTakeoffAir() self:SetLowFuelThreshold() + self:SetRespawnOnOff() ----------------------- --- FSM Transitions --- @@ -123,10 +129,11 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") - self:AddTransition("Running", "RTB", "Returning") - self:AddTransition("Running", "Status", "*") - self:AddTransition("Returning", "Status", "*") - self:AddTransition("Running", "Stop", "Stopped") + self:AddTransition("*", "Refuel", "Refueling") + self:AddTransition("*", "Run", "Running") + self:AddTransition("Running", "RTB", "Returning") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "Stopped") --- Triggers the FSM event "Start" that starts the recovery tanker. Initializes parameters and starts event handlers. @@ -138,6 +145,29 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- @param #RECOVERYTANKER self -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Refuel" when the tanker is refueling another aircraft. + -- @function [parent=#RECOVERYTANKER] Refuel + -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. + -- @param #RECOVERYTANKER self + + --- Triggers delayed the FSM event "Refuel" when the tanker is refueling another aircraft. + -- @function [parent=#RECOVERYTANKER] __Refuel + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. + + + --- Triggers the FSM event "Run". Simply puts the group into "Running" state, e.g. after refueling ended. + -- @function [parent=#RECOVERYTANKER] Run + -- @param #RECOVERYTANKER self + + --- Triggers delayed the FSM event "Run". Simply puts the group into "Running" state, e.g. after refueling ended. + -- @function [parent=#RECOVERYTANKER] __Run + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "RTB" that sends the tanker home. -- @function [parent=#RECOVERYTANKER] RTB -- @param #RECOVERYTANKER self @@ -147,6 +177,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- @param #RECOVERYTANKER self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Stop" that stops the recovery tanker. Event handlers are stopped. -- @function [parent=#RECOVERYTANKER] Stop -- @param #RECOVERYTANKER self @@ -254,6 +285,55 @@ function RECOVERYTANKER:SetTakeoffAir() end +--- Enable respawning of tanker. Note that this is the default behaviour. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOn() + self.respawn=true + return self +end + +--- Disable respawning of tanker. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOff() + self.respawn=false + return self +end + +--- Set whether tanker shall be respawned or not. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true (or nil), tanker will be respawned. If false, tanker will not be respawned. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOnOff(switch) + if switch==nil or switch==true then + self.respawn=true + else + self.respawn=false + end + return self +end + +--- Tanker will be respawned in air, even it was initially spawned on the carrier. +-- So only the first spawn will be on the carrier while all subsequent spawns will happen in air. +-- This allows for undisrupted operations and less problems on the carrier deck. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnInAir() + self.respawninair=true + return self +end + +--- Use an uncontrolled aircraft already present in the mission rather than spawning a new tanker as initial recovery thanker. +-- This can be useful when interfaced with, e.g., a warehouse. +-- The group name is the one specified in the @{#RECOVERYTANKER.New} function. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetUseUncontrolledAircraft() + self.uncontrolledac=true + return self +end + --- Check if tanker is returning to base. -- @param #RECOVERYTANKER self -- @return #boolean If true, tanker is returning to base. @@ -284,7 +364,9 @@ function RECOVERYTANKER:onafterStart(From, Event, To) -- Handle events. self:HandleEvent(EVENTS.EngineShutdown) - --TODO: Handle event crash and respawn. + self:HandleEvent(EVENTS.Refueling) + self:HandleEvent(EVENTS.RefuelingStop) + self:HandleEvent(EVENTS.Crash) -- Spawn tanker. local Spawn=SPAWN:New(self.tankergroupname):InitUnControlled(false) @@ -295,6 +377,7 @@ function RECOVERYTANKER:onafterStart(From, Event, To) -- Carrier heading local hdg=self.carrier:GetHeading() + -- Spawn distance behind the carrier. local dist=UTILS.NMToMeters(20) -- Coordinate behind the carrier @@ -306,11 +389,32 @@ function RECOVERYTANKER:onafterStart(From, Event, To) -- Spawn at coordinate. self.tanker=Spawn:SpawnFromCoordinate(Carrier) + -- Initial route. self:_InitRoute(15, 1, 2) else - -- Spawn tanker at airbase. - self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) + -- Check if an uncontrolled tanker group was requested. + if self.useuncontrolled then + + -- Use an uncontrolled aircraft group. + self.tanker=GROUP:FindByName(self.tankergroupname) + + if self.tanker:IsAlive() then + -- Start uncontrolled group. + self.tanker:StartUncontrolled() + else + self:E(string.format("ERROR: No uncontrolled (alive) tanker group with name %s could be found!", self.tankergroupname)) + return + end + + else + + -- Spawn tanker at airbase. + self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) + + end + + -- Initialize route. self:_InitRoute(30, 10, 1) end @@ -331,10 +435,11 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) -- Get fuel of tanker. local fuel=self.tanker:GetFuel()*100 - local text=string.format("Tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) + local text=string.format("Recovery tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) self:I(text) + -- Check if tanker is running and not RTBing. if self:IsRunning() then -- Check fuel. @@ -350,6 +455,7 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) --Time since last pattern update. local dt=time-self.Tupdate + -- Update pattern. if dt>self.dTupdate then self:_PatternUpdate() end @@ -363,16 +469,6 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) self:__Status(-60) end ---- On after Stop event. Unhandle events and stop status updates. --- @param #RECOVERYTANKER self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function RECOVERYTANKER:onafterStop(From, Event, To) - self:UnHandleEvent(EVENTS.EngineShutdown) - --self:UnHandleEvent(EVENTS.Land) -end - --- On before RTB event. Check if takeoff type is air and if so respawn the tanker and deny RTB transition. -- @param #RECOVERYTANKER self -- @param #string From From state. @@ -381,27 +477,32 @@ end -- @return #boolean If true, transition is allowed. function RECOVERYTANKER:onbeforeRTB(From, Event, To) + -- Check if spawn in air is activated. if self.takeoff==SPAWN.Takeoff.Air then - -- Debug message. - local text=string.format("Respawning tanker %s.", self.tanker:GetName()) - self:I(text) - - -- Respawn tanker. - self.tanker:InitHeading(self.tanker:GetHeading()) - self.tanker=self.tanker:Respawn(nil, true) - - -- Update Pattern in 2 seconds. Need to give a bit time so that the respawned group is in the game. - SCHEDULER:New(nil, self._PatternUpdate, {self}, 2) - - -- Deny transition to RTB. - return false + -- Check that respawn should happen. + if self.respawn then + + -- Debug message. + local text=string.format("Respawning tanker %s.", self.tanker:GetName()) + self:I(text) + + -- Respawn tanker. + self.tanker:InitHeading(self.tanker:GetHeading()) + self.tanker=self.tanker:Respawn(nil, true) + + -- Update Pattern in 2 seconds. Need to give a bit time so that the respawned group is in the game. + SCHEDULER:New(nil, self._PatternUpdate, {self}, 2) + + -- Deny transition to RTB. + return false + end end return true end ---- On after RTB event. Send tanker back to carrier. +--- On after "RTB" event. Send tanker back to carrier. -- @param #RECOVERYTANKER self -- @param #string From From state. -- @param #string Event Event. @@ -412,17 +513,28 @@ function RECOVERYTANKER:onafterRTB(From, Event, To) local text=string.format("Tanker %s returning to airbase %s.", self.tanker:GetName(), self.airbase:GetName()) self:I(text) - local waypoints={} + -- Waypoint array. + local wp={} - -- Set landingwaypoint - local wp=self.carrier:GetCoordinate():WaypointAirLanding(300, self.airbase, nil, "Landing") - table.insert(waypoints, wp) + -- Set landing waypoint. + wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil, 300, {}, "Current Position") + wp[2]=self.carrier:GetCoordinate():WaypointAirLanding(300, self.airbase, nil, "Landing on Carrier") -- Initialize WP and route tanker. - self.tanker:WayPointInitialize(waypoints) + self.tanker:WayPointInitialize(wp) -- Set task. - self.tanker:Route(waypoints, 1) + self.tanker:Route(wp, 1) +end + +--- On after Stop event. Unhandle events and stop status updates. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RECOVERYTANKER:onafterStop(From, Event, To) + self:UnHandleEvent(EVENTS.EngineShutdown) + --self:UnHandleEvent(EVENTS.Land) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -437,7 +549,8 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) local group=EventData.IniGroup --Wrapper.Group#GROUP - if group:IsAlive() then + -- Check if group is alive and should be respawned. + if group:IsAlive() and self.respawn then -- Group name. When spawning it will have #001 attached. local groupname=group:GetName() @@ -449,9 +562,7 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) -- Respawn tanker. self.tanker=group:RespawnAtCurrentAirbase() - - --group:StartUncontrolled(60) - + -- Initial route. self:_InitRoute() end @@ -459,6 +570,57 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) end end +--- Event handler for refueling started. +-- @param #RECOVERYTANKER self +-- @param Core.Event#EVENTDATA EventData Event data. +function RECOVERYTANKER:OnEventRefuel(EventData) + + if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() then + + -- Unit receiving fuel. + local unit=EventData.IniUnit + + -- Get distance to tanker to check that unit is receiving fuel from this tanker. + local dist=unit:GetCoordinate():Get2DDistance(self.tanker:GetCoordinate()) + + -- If distance > 100 meters, this should be another tanker. + if dist>100 then + return + end + + -- Info message. + self:I(string.format("Recovery tanker %s started refueling unit %s", self.tanker:GetName(), unit:GetName())) + + end + +end + +--- Event handler for refueling stopped. +-- @param #RECOVERYTANKER self +-- @param Core.Event#EVENTDATA EventData Event data. +function RECOVERYTANKER:OnEventRefuelStop(EventData) + + if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() then + + -- Unit receiving fuel. + local unit=EventData.IniUnit + + -- Get distance to tanker to check that unit is receiving fuel from this tanker. + local dist=unit:GetCoordinate():Get2DDistance(self.tanker:GetCoordinate()) + + -- If distance > 100 meters, this should be another tanker. + if dist>100 then + return + end + + -- Info message. + self:I(string.format("Recovery tanker %s stopped refueling unit %s", self.tanker:GetName(), unit:GetName())) + + end + +end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ROUTE functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -476,7 +638,7 @@ function RECOVERYTANKER:_InitRoute(dist, Tstart, delay) delay=delay or 1 -- Debug message. - self:I(string.format("Initializing route for tanker %s.", self.tanker:GetName())) + self:I(string.format("Initializing route for recovery tanker %s.", self.tanker:GetName())) -- Carrier position. local Carrier=self.carrier:GetCoordinate() @@ -509,6 +671,9 @@ end --- Function to update the race-track pattern of the tanker wrt to the carrier position. -- @param #RECOVERYTANKER self function RECOVERYTANKER:_PatternUpdate() + + -- Debug message. + self:I(string.format("Updating recovery tanker %s orbit.", self.tanker:GetName())) -- Carrier heading. local hdg=self.carrier:GetHeading() @@ -517,38 +682,34 @@ function RECOVERYTANKER:_PatternUpdate() local Carrier=self.carrier:GetCoordinate() -- Define race-track pattern. + local p0=self.tanker:GetCoordinate():Translate(1000, self.tanker:GetHeading()) local p1=Carrier:SetAltitude(self.altitude):Translate(self.distStern, hdg) local p2=Carrier:SetAltitude(self.altitude):Translate(self.distBow, hdg) -- Set orbit task. local taskorbit=self.tanker:TaskOrbit(p1, self.altitude, self.speed, p2) - -- New waypoint. - local p0=self.tanker:GetCoordinate():Translate(1000, self.tanker:GetHeading()) - -- Debug markers. if self.Debug then - p0:MarkToAll("p0") - p1:MarkToAll("p1") - p2:MarkToAll("p2") + p0:MarkToAll("Waypoint P0 " ..self.tanker:GetName()) + p1:MarkToAll("Racetrack P1 "..self.tanker:GetName()) + p2:MarkToAll("Racetrack P2 "..self.tanker:GetName()) end - - -- Debug message. - self:I(string.format("Updating tanker %s orbit.", self.tanker:GetName())) - + -- Waypoints array. - local waypoints={} + local wp={} -- New waypoint with orbit pattern task. - local wp=p0:WaypointAirTurningPoint(nil, self.speed, {taskorbit}, "Tanker Orbit") - waypoints[1]=wp + wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil , self.speed, {}, "Current Position") + wp[2]=p0:WaypointAirTurningPoint(nil, self.speed, {taskorbit}, "Tanker Orbit") -- Initialize WP and route tanker. - self.tanker:WayPointInitialize(waypoints) + self.tanker:WayPointInitialize(wp) -- Task combo. local tasktanker = self.tanker:EnRouteTaskTanker() - local taskroute = self.tanker:TaskRoute(waypoints) + local taskroute = self.tanker:TaskRoute(wp) + -- Note that tasktanker has to come first. Otherwise it does not work! local taskcombo = self.tanker:TaskCombo({tasktanker, taskroute}) -- Set task. diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 6272d32e3..6e8a83d1d 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -1,11 +1,13 @@ --- **Functional** - (R2.5) - Rescue helo. -- --- Recue helicopter on an aircraft carrier. +-- Recue helicopter for carrier operations. -- -- Features: -- --- * Formation with carrier. --- * Automatic respawning on empty fuel. +-- * Close formation with carrier. +-- * Carrier can have any number of waypoints. +-- * Automatic respawning on empty fuel for 24/7 operations. +-- * Automatic rescuing of crashed or ejected units in the vicinity. -- -- Please not that his class is work in progress and in an **alpha** stage. -- @@ -24,13 +26,14 @@ -- @field #string helogroupname Name of the late activated helo template group. -- @field Wrapper.Group#GROUP helo Helo group. -- @field #number takeoff Takeoff type. --- @field Wrapper.Airbase#AIRBASE airbase The airbase object of the carrier. +-- @field Wrapper.Airbase#AIRBASE airbase The airbase object acting as home base of the helo. -- @field Core.Set#SET_GROUP followset Follow group set. -- @field AI.AI_Formation#AI_FORMATION formation AI_FORMATION object. -- @field #number lowfuel Low fuel threshold of helo in percent. -- @field #number altitude Altitude of helo in meters. -- @field #number offsetX Offset in meters to carrier in longitudinal direction. -- @field #number offsetZ Offset in meters to carrier in latitudinal direction. +-- @field Core.Zone#ZONE_RADIUS rescuezone Zone around the carrier in which helo will rescue crashed or ejected units. -- @extends Core.Fsm#FSM --- Rescue Helo @@ -45,7 +48,7 @@ -- -- @field #RESCUEHELO RESCUEHELO = { - ClassName = "RESCUEHELO", + ClassName = "RESCUEHELO", carrier = nil, carriertype = nil, helogroupname = nil, @@ -58,20 +61,22 @@ RESCUEHELO = { altitude = nil, offsetX = nil, offsetZ = nil, + rescuezone = nil, } --- Class version. -- @field #string version -RESCUEHELO.version="0.9.1" +RESCUEHELO.version="0.9.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Add option to stop carrier while rescue operation is in progress. -- TODO: Possibility to add already present/spawned aircraft, e.g. for warehouse. -- TODO: Write documenation. --- TODO: Add rescue event when aircraft crashes. --- TODO: Make offset input parameter. +-- DONE: Add rescue event when aircraft crashes. +-- DONE: Make offset input parameter. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -87,6 +92,7 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- Inherit everthing from FSM class. local self = BASE:Inherit(self, FSM:New()) -- #RESCUEHELO + -- Catch case when just the unit name is passed. if type(carrierunit)=="string" then self.carrier=UNIT:FindByName(carrierunit) else @@ -98,10 +104,7 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- Helo group name. self.helogroupname=helogroupname - - -- Home airbase of helo - self.airbase=AIRBASE:FindByName(self.carrier:GetName()) - + -- Init defaults. self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) self:SetTakeoffHot() @@ -109,6 +112,7 @@ function RESCUEHELO:New(carrierunit, helogroupname) self:SetAltitude() self:SetOffsetX() self:SetOffsetZ() + self:SetRescueZone() ----------------------- --- FSM Transitions --- @@ -120,10 +124,11 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("Running", "Rescue", "Rescuing") self:AddTransition("Running", "RTB", "Returning") - self:AddTransition("Returning", "Status", "*") - self:AddTransition("Running", "Status", "*") - self:AddTransition("Running", "Stop", "Stopped") + self:AddTransition("*", "Run", "Running") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "Stopped") --- Triggers the FSM event "Start" that starts the rescue helo. Initializes parameters and starts event handlers. @@ -135,6 +140,17 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- @param #RESCUEHELO self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate. + -- @function [parent=#RESCUEHELO] Rescue + -- @param #RESCUEHELO self + -- @param Core.Point#COORDINATE RescueCoord Coordinate where the resue mission takes place. + + --- Triggers the delayed FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate. + -- @function [parent=#RESCUEHELO] __Rescue + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + -- @param Core.Point#COORDINATE RescueCoord Coordinate where the resue mission takes place. + --- Triggers the FSM event "RTB" that sends the helo home. -- @function [parent=#RESCUEHELO] RTB -- @param #RESCUEHELO self @@ -144,6 +160,15 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- @param #RESCUEHELO self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Run". + -- @function [parent=#RESCUEHELO] Run + -- @param #RESCUEHELO self + + --- Triggers the delayed FSM event "Run". + -- @function [parent=#RESCUEHELO] __Run + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped. -- @function [parent=#RESCUEHELO] Stop -- @param #RESCUEHELO self @@ -178,12 +203,21 @@ function RESCUEHELO:SetHomeBase(airbase) return self end +--- Set rescue zone radius. Crashed or ejected units inside this radius of the carrier will be rescued. +-- @param #RESCUEHELO self +-- @param #number radius Radius of rescue zone in meters. Default is 100000 m = 100 km. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueZone(radius) + self.rescuezone=ZONE_UNIT:New("Rescue Zone", self.carrier, radius or 100000) + return self +end + --- Set takeoff type. -- @param #RESCUEHELO self --- @param #number takeofftype Takeoff type. +-- @param #number takeofftype Takeoff type. Default SPAWN.Takeoff.Hot. -- @return #RESCUEHELO self function RESCUEHELO:SetTakeoff(takeofftype) - self.takeoff=takeofftype + self.takeoff=takeofftype or SPAWN.Takeoff.Hot return self end @@ -239,20 +273,109 @@ function RESCUEHELO:SetOffsetZ(distance) end ---- Check if tanker is returning to base. +--- Check if helo is returning to base. -- @param #RESCUEHELO self -- @return #boolean If true, helo is returning to base. function RESCUEHELO:IsReturning() return self:is("Returning") end ---- Check if tanker is operating. +--- Check if helo is operating. -- @param #RESCUEHELO self -- @return #boolean If true, helo is operating. function RESCUEHELO:IsRunning() return self:is("Running") end +--- Check if helo is on a rescue mission. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is rescuing somebody. +function RESCUEHELO:IsRescuing() + return self:is("Rescuing") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Handle landing event of rescue helo. +-- @param #RESCUEHELO self +-- @param Core.Event#EVENTDATA EventData Event data. +function RESCUEHELO:OnEventLand(EventData) + local group=EventData.IniGroup --Wrapper.Group#GROUP + + if group:IsAlive() then + local groupname=group:GetName() + + if groupname:match(self.helogroupname) then + + -- Respawn the Helo. + self:I(string.format("Respawning rescue helo group %s at home base.", groupname)) + + if self.takeoff==SPAWN.Takeoff.Air then + + self:E("ERROR: Rescue helo %s landed. This should not happen for Takeoff=Air!", groupname) + + else + + -- Respawn helo at current airbase. + self.helo=group:RespawnAtCurrentAirbase() + + end + + -- Restart the formation. + self:__Run(10) + end + end +end + +--- A unit crashed or a player ejected. +-- @param #RESCUEHELO self +-- @param Core.Event#EVENTDATA EventData Event data. +function RESCUEHELO:_OnEventCrashOrEject(EventData) + self:F2({eventdata=EventData}) + + -- NOTE: Careful here. Eject and crash events will probably happen for the same unit! + + -- Check that there is an initiating unit in the event data. + if EventData and EventData.IniUnit then + + -- Crashed or ejected unit. + local unit=EventData.IniUnit + local unitname=tostring(EventData.IniUnitName) + + -- Check that it was not the rescue helo itself that crashed. + if EventData.IniGroupName~=self.helo:GetName() then + + -- Debug. + self:T(string.format("Unit %s crashed or ejected.", unitname)) + + -- Unit "alive" and in our rescue zone. + if unit:IsAlive() and unit:IsInZone(self.rescuezone) then + + -- Get coordinate of crashed unit. + local coord=unit:GetCoordinate() + + -- Debug mark on map. + coord:MarkToCoalition(string.format("Crash site of unit %s.", unitname), self.helo:GetCoalition()) + + -- Only rescue if helo is "running" and not, e.g., rescuing already. + if self:IsRunning() then + self:Rescue(coord) + end + + end + + else + + self:I(string.format("Rescue helo %s crashed!", unitname)) + + end + + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM states ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -270,7 +393,8 @@ function RESCUEHELO:onafterStart(From, Event, To) -- Handle events. --self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Land) - --self:HandleEvent(EVENTS.Crash) + self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrEject) + self:HandleEvent(EVENTS.Ejection, self._OnEventCrashOrEject) -- Delay before formation is started. local delay=120 @@ -356,7 +480,7 @@ function RESCUEHELO:onafterStatus(From, Event, To) self:I(text) -- If fuel < threshold ==> send helo to home base! - if fuel Date: Tue, 20 Nov 2018 20:00:38 +0100 Subject: [PATCH 049/147] A lot of optimizations ... - Depart from runway now works again. - Depart from hotspot works again. - Depart from ramp works again. - Implemented the limit, it works, but it is not waterproof (yet). --- .../Moose/AI/AI_A2G_Dispatcher.lua | 248 ++++++++++++++---- 1 file changed, 196 insertions(+), 52 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 1339e63f0..fde91d028 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -595,6 +595,8 @@ do -- AI_A2G_DISPATCHER self.Detection = Detection -- Functional.Detection#DETECTION_AREAS + self.Detection:FilterCategories( Unit.Category.GROUND_UNIT ) + -- This table models the DefenderSquadron templates. self.DefenderSquadrons = {} -- The Defender Squadrons. self.DefenderSpawns = {} @@ -1086,6 +1088,27 @@ do -- AI_A2G_DISPATCHER end + --- Set the default engage limit for squadrons, which will be used to determine how many air units will engage at the same time with the enemy. + -- The default eatrol limit is 1, which means one eatrol group maximum per squadron. + -- @param #AI_A2G_DISPATCHER self + -- @param #number EngageLimit The maximum engages that can be done at the same time per squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol limit. + -- A2GDispatcher:SetDefaultEngageLimit( 2 ) -- Maximum 2 engagements with the enemy per squadron. + -- + function AI_A2G_DISPATCHER:SetDefaultEngageLimit( EngageLimit ) + + self.DefenderDefault.EngageLimit = EngageLimit + + return self + end + + function AI_A2G_DISPATCHER:SetIntercept( InterceptDelay ) self.DefenderDefault.InterceptDelay = InterceptDelay @@ -1202,7 +1225,7 @@ do -- AI_A2G_DISPATCHER --- -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target ) + function AI_A2G_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target, Size ) self:F( { SquadronName = SquadronName, Defender = Defender:GetName() } ) @@ -1210,6 +1233,7 @@ do -- AI_A2G_DISPATCHER self.DefenderTasks[Defender].Type = Type self.DefenderTasks[Defender].Fsm = Fsm self.DefenderTasks[Defender].SquadronName = SquadronName + self.DefenderTasks[Defender].Size = Size if Target then self:SetDefenderTaskTarget( Defender, Target ) @@ -1453,6 +1477,8 @@ do -- AI_A2G_DISPATCHER end + + --- Set the squadron Patrol parameters for SEAD tasks. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -1590,6 +1616,39 @@ do -- AI_A2G_DISPATCHER return nil end + --- Set the squadron engage limit for a specific task type. + -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. + -- + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for SEAD tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for CAS tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for BAI tasks. + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronEngageLimit( "Mineralnye", 2, "SEAD" ) -- Engage maximum 2 groups with the enemy for SEAD defense. + -- + function AI_A2G_DISPATCHER:SetSquadronEngageLimit( SquadronName, EngageLimit, DefenseTaskType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Defense = DefenderSquadron[DefenseTaskType] + if Defense then + Defense.EngageLimit = EngageLimit or 1 + else + error( "This squadron does not exist:" .. SquadronName ) + end + + end + + + --- -- @param #AI_A2G_DISPATCHER self @@ -1618,6 +1677,25 @@ do -- AI_A2G_DISPATCHER self:F( { Sead = Sead } ) end + + --- Set the squadron SEAD engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for SEAD defense. + -- + function AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "SEAD" ) + + end + + --- Set a Sead patrol for a Squadron. @@ -1690,6 +1768,26 @@ do -- AI_A2G_DISPATCHER self:F( { Cas = Cas } ) end + + + --- Set the squadron CAS engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for CAS defense. + -- + function AI_A2G_DISPATCHER:SetSquadronCasEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "CAS" ) + + end + + --- Set a Cas patrol for a Squadron. @@ -1762,6 +1860,24 @@ do -- AI_A2G_DISPATCHER self:F( { Bai = Bai } ) end + + + --- Set the squadron BAI engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for BAI defense. + -- + function AI_A2G_DISPATCHER:SetSquadronBaiEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "BAI" ) + + end --- Set a Bai patrol for a Squadron. @@ -2624,9 +2740,12 @@ do -- AI_A2G_DISPATCHER function AI_A2G_DISPATCHER:CountDefendersEngaged( AttackerDetection ) -- First, count the active AIGroups Units, targetting the DetectedSet - local DefenderCount = 0 + local DefendersEngaged = 0 + local DefendersTotal = 0 - local DetectedSet = AttackerDetection.Set + local AttackerSet = AttackerDetection.Set + local AttackerCount = AttackerSet:Count() + local DefendersMissing = AttackerCount --DetectedSet:Flush() local DefenderTasks = self:GetDefenderTasks() @@ -2634,24 +2753,34 @@ do -- AI_A2G_DISPATCHER local Defender = DefenderGroup -- Wrapper.Group#GROUP local DefenderTaskTarget = DefenderTask.Target local DefenderSquadronName = DefenderTask.SquadronName - - if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then - - local SquadronOverhead = self:GetSquadronOverhead( DefenderSquadronName ) - - local DefenderSize = Defender:GetInitialSize() - if DefenderSize then - DefenderCount = DefenderCount + DefenderSize / SquadronOverhead + local DefenderSize = DefenderTask.Size + + -- Count the total of defenders on the battlefield. + --local DefenderSize = Defender:GetInitialSize() + if DefenderTask.Target then + --if DefenderTask.Fsm:Is( "Engaging" ) then self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) - else - DefenderCount = 0 - end + DefendersTotal = DefendersTotal + DefenderSize + if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then + + local SquadronOverhead = self:GetSquadronOverhead( DefenderSquadronName ) + if DefenderSize then + DefendersEngaged = DefendersEngaged + DefenderSize + DefendersMissing = DefendersMissing - DefenderSize / SquadronOverhead + self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + else + DefendersEngaged = 0 + end + end + --end end + + end - self:F( { DefenderCount = DefenderCount } ) + self:F( { DefenderCount = DefendersEngaged } ) - return DefenderCount + return DefendersTotal, DefendersEngaged, DefendersMissing end --- @@ -2860,9 +2989,9 @@ do -- AI_A2G_DISPATCHER --- -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:onafterDefend( From, Event, To, AttackerDetection, DefendersMissing, DefenderFriendlies, DefenseTaskType ) + function AI_A2G_DISPATCHER:onafterDefend( From, Event, To, AttackerDetection, DefendersTotal, DefendersEngaged, DefendersMissing, DefenderFriendlies, DefenseTaskType ) - self:F( { From, Event, To, AttackerDetection.Index, DefendersMissing, DefenderFriendlies } ) + self:F( { From, Event, To, AttackerDetection.Index, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing, DefenderFriendlies = DefenderFriendlies } ) local AttackerSet = AttackerDetection.Set local AttackerUnit = AttackerSet:GetFirst() @@ -2875,13 +3004,15 @@ do -- AI_A2G_DISPATCHER local SquadronName = self:GetDefenderTask( DefenderGroup ).SquadronName local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) - + local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) Fsm:__Engage( 1, AttackerSet ) -- Engage on the TargetSetUnit self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) - DefendersMissing = DefendersMissing - DefenderGroup:GetSize() / SquadronOverhead + local DefenderGroupSize = DefenderGroup:GetSize() + DefendersMissing = DefendersMissing - DefenderGroupSize / SquadronOverhead + DefendersTotal = DefendersTotal + DefenderGroupSize / SquadronOverhead if DefendersMissing <= 0 then break @@ -2938,6 +3069,22 @@ do -- AI_A2G_DISPATCHER self:F( { Overhead = DefenderOverhead, SquadronOverhead = DefenderSquadron.Overhead , DefaultOverhead = self.DefenderDefault.Overhead } ) self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } ) self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } ) + + -- Validate that the maximum limit of Defenders has been reached. + -- If yes, then cancel the engaging of more defenders. + local DefendersLimit = DefenderSquadron.EngageLimit or self.DefenderDefault.EngageLimit + if DefendersLimit then + if DefendersTotal >= DefendersLimit then + DefendersNeeded = 0 + BreakLoop = true + else + -- If the total of amount of defenders + the defenders needed, is larger than the limit of defenders, + -- then the defenders needed is the difference between defenders total - defenders limit. + if DefendersTotal + DefendersNeeded > DefendersLimit then + DefendersNeeded = DefendersLimit - DefendersTotal + end + end + end -- DefenderSquadron.ResourceCount can have the value nil, which expresses unlimited resources. -- DefendersNeeded cannot exceed DefenderSquadron.ResourceCount! @@ -2964,9 +3111,7 @@ do -- AI_A2G_DISPATCHER Fsm:SetDisengageRadius( self.DisengageRadius ) Fsm:Start() - - self:SetDefenderTask( ClosestDefenderSquadronName, DefenderGroup, DefenseTaskType, Fsm, AttackerDetection ) - + self:SetDefenderTask( ClosestDefenderSquadronName, DefenderGroup, DefenseTaskType, Fsm, AttackerDetection, DefenderGrouping ) function Fsm:onafterTakeoff( Defender, From, Event, To ) self:F({"Defender Birth", Defender:GetName()}) @@ -3049,25 +3194,24 @@ do -- AI_A2G_DISPATCHER local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT local AttackerCount = AttackerSet:Count() - local IsSEAD = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group?µ + local IsSEAD = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group? if ( IsSEAD > 0 ) then -- First, count the active defenders, engaging the DetectedItem. - local DefenderCount = self:CountDefendersEngaged( DetectedItem ) + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) + + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) - local DefendersMissing = AttackerCount - DefenderCount - self:F( { AttackerCount = AttackerCount, DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) - - local DefenderGroups = self:CountDefenders( DetectedItem, DefenderCount, "SEAD" ) + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "SEAD" ) if DetectedItem.IsDetected == true then - return DefendersMissing, DefenderGroups + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups end end - return nil, nil + return nil, nil, nil end @@ -3090,20 +3234,19 @@ do -- AI_A2G_DISPATCHER if IsCas == true then -- First, count the active defenders, engaging the DetectedItem. - local DefenderCount = self:CountDefendersEngaged( DetectedItem ) + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) - local DefendersMissing = AttackerCount - DefenderCount - self:F( { AttackerCount = AttackerCount, DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) - local DefenderGroups = self:CountDefenders( DetectedItem, DefenderCount, "CAS" ) + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "CAS" ) if DetectedItem.IsDetected == true then - return DefendersMissing, DefenderGroups + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups end end - return nil, nil + return nil, nil, nil end @@ -3124,20 +3267,19 @@ do -- AI_A2G_DISPATCHER if IsBai == true then -- First, count the active defenders, engaging the DetectedItem. - local DefenderCount = self:CountDefendersEngaged( DetectedItem ) + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) - local DefendersMissing = AttackerCount - DefenderCount - self:F( { AttackerCount = AttackerCount, DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) - local DefenderGroups = self:CountDefenders( DetectedItem, DefenderCount, "BAI" ) + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "BAI" ) if DetectedItem.IsDetected == true then - return DefendersMissing, DefenderGroups + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups end end - return nil, nil + return nil, nil, nil end @@ -3186,6 +3328,8 @@ do -- AI_A2G_DISPATCHER local DefenderGroupCount = 0 local Delay = 0 -- We need to implement a delay for each action because the spawning on airbases get confused if done too quick. + local DefendersTotal = 0 + -- Now that all obsolete tasks are removed, loop through the detected targets. for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do @@ -3229,28 +3373,28 @@ do -- AI_A2G_DISPATCHER if DefenseCoordinate then do - local DefendersMissing, Friendlies = self:Evaluate_SEAD( DetectedItem ) -- Returns a SET_UNIT with the SEAD targets to be engaged... + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_SEAD( DetectedItem ) -- Returns a SET_UNIT with the SEAD targets to be engaged... if DefendersMissing and DefendersMissing > 0 then - self:F( { SeadGroups = Friendlies } ) - self:__Defend( Delay, DetectedItem, DefendersMissing, Friendlies, "SEAD", DefenseCoordinate ) + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "SEAD", DefenseCoordinate ) Delay = Delay + 1 end end do - local DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... if DefendersMissing and DefendersMissing > 0 then - self:F( { CasGroups = Friendlies } ) - self:__Defend( Delay, DetectedItem, DefendersMissing, Friendlies, "CAS", DefenseCoordinate ) + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "CAS", DefenseCoordinate ) Delay = Delay + 1 end end do - local DefendersMissing, Friendlies = self:Evaluate_BAI( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_BAI( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... if DefendersMissing and DefendersMissing > 0 then - self:F( { BaiGroups = Friendlies } ) - self:__Defend( Delay, DetectedItem, DefendersMissing, Friendlies, "BAI", DefenseCoordinate ) + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "BAI", DefenseCoordinate ) Delay = Delay + 1 end end From ba1cb61f4db94c0c6291cce5a195e42d1ab01aca Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Nov 2018 00:55:00 +0100 Subject: [PATCH 050/147] AIRBOSS v0.3.1 Tanker v0.9.3 * added TACAN option Helo v0.9.3 * added respawn and uncontrolled --- .../Moose/Functional/Warehouse.lua | 2 +- Moose Development/Moose/Ops/Airboss.lua | 703 ++++++++++++++---- .../Moose/Ops/RecoveryTanker.lua | 40 +- Moose Development/Moose/Ops/RescueHelo.lua | 144 +++- 4 files changed, 699 insertions(+), 190 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 556d93afd..f65037e6c 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1800,7 +1800,7 @@ function WAREHOUSE:New(warehouse, alias) if type(warehouse)=="string" then warehouse=UNIT:FindByName(warehouse) if warehouse==nil then - env.info(string.format("FF no warehouse unit with name %s found trying static.", warehouse)) + env.info(string.format("No warehouse unit with name %s found trying static.", warehouse)) warehouse=STATIC:FindByName(warehouse, true) end end diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 38b52fd46..f61d9442f 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -5,7 +5,7 @@ -- Features: -- -- * CASE I, II and III recoveries. --- * Supports human pilots as well as AI. +-- * Supports human pilots as well as AI flight groups. -- * Automatic LSO grading. -- * Different skill levels from tipps on-the-fly for students to complete ziplip for pros. -- * Rescue helo option. @@ -16,7 +16,7 @@ -- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels, LSO grades). -- * Multiple carriers supported (due to object oriented approach). -- --- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage. +-- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much work in progress. -- At the moment training parameters are optimized for F/A-18C Hornet as aircraft and USS John C. Stennis as carrier. -- Other aircraft and carriers **might** be possible in future but would need a different set of parameters. -- @@ -356,30 +356,6 @@ AIRBOSS.GroovePos={ -- @field #number points Points received. -- @field #string details Detailed flight analyis analysis. ---- Player data table holding all important parameters of each player. --- @type AIRBOSS.PlayerData --- @field Wrapper.Unit#UNIT unit Aircraft of the player. --- @field #string name Player name. --- @field Wrapper.Client#CLIENT client Client object of player. --- @field Wrapper.Group#GROUP group Aircraft group the player is in. --- @field #string callsign Callsign of player. --- @field #string difficulty Difficulty level. --- @field #string step Coming pattern step. --- @field #number passes Number of passes. --- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. --- @field #table debrief Debrief analysis of the current step of this pass. --- @field #table grades LSO grades of player passes. --- @field #boolean holding If true, player is in holding zone. --- @field #boolean landed If true, player landed or attempted to land. --- @field #boolean bolter If true, LSO told player to bolter. --- @field #boolean boltered If true, player boltered. --- @field #boolean waveoff If true, player was waved off during final approach. --- @field #boolean patternwo If true, player was waved of during the pattern. --- @field #boolean lig If true, player was long in the groove. --- @field #number Tlso Last time the LSO gave an advice. --- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. --- @field #table menu F10 radio menu - --- Checkpoint parameters triggering the next step in the pattern. -- @type AIRBOSS.Checkpoint -- @field #string name Name of checkpoint. @@ -401,19 +377,46 @@ AIRBOSS.GroovePos={ -- @field #number Speed Optimal speed at this point. -- @field #table Checklist Table of checklist text items to display at this point. ---- Marshal and pattern queue items. +--- Player data table holding all important parameters of each player. +-- @type AIRBOSS.PlayerData +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string name Player name. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field Wrapper.Group#GROUP group Aircraft group the player is in. +-- @field #string callsign Callsign of player. +-- @field #string actype Aircraft type. +-- @field #string onboard Onboard number. +-- @field #string difficulty Difficulty level. +-- @field #string step Coming pattern step. +-- @field #number passes Number of passes. +-- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. +-- @field #table debrief Debrief analysis of the current step of this pass. +-- @field #table grades LSO grades of player passes. +-- @field #boolean holding If true, player is in holding zone. +-- @field #boolean landed If true, player landed or attempted to land. +-- @field #boolean bolter If true, LSO told player to bolter. +-- @field #boolean boltered If true, player boltered. +-- @field #boolean waveoff If true, player was waved off during final approach. +-- @field #boolean patternwo If true, player was waved of during the pattern. +-- @field #boolean lig If true, player was long in the groove. +-- @field #number Tlso Last time the LSO gave an advice. +-- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. +-- @field #string seclead Name of section lead. +-- @field #table menu F10 radio menu + +--- Parameters of a flight group. -- @type AIRBOSS.Flightitem -- @field Wrapper.Group#GROUP group Flight group. -- @field #string groupname Name of the group. -- @field #number nunits Number of units in group. -- @field #number dist0 Distance to carrier in meters when the group was first detected inside the CCA. --- @field #number fuel Fuel state. -- @field #number time Time the flight was added to the queue. -- @field Core.UserFlag#USERFLAG flag User flag for triggering events for the flight. -- @field #boolean ai If true, flight is AI. If false, flight is a human player. -- @field #AIRBOSS.PlayerData player Player data for human pilots. -- @field #string actype Aircraft type name. -- @field #table onboardnumbers Onboard numbers of aircraft in the group. +-- @field #table section Other human flight groups belonging to this flight. This flight is the lead. --- Main radio menu. -- @field #table MenuF10 @@ -421,7 +424,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.0" +AIRBOSS.version="0.3.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1153,7 +1156,7 @@ function AIRBOSS:_InitStennis() end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Queues +-- QUEUE Functions ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Check marshal and pattern queues. @@ -1169,41 +1172,50 @@ function AIRBOSS:_CheckQueue() end -- Print queues. + self:_PrintQueue(self.flights, "All Flights") self:_PrintQueue(self.Qmarshal, "Marshal") self:_PrintQueue(self.Qpattern, "Pattern") - -- Collapse marshal stack. + -- Check if there are flights in marshal strack and if the pattern is free. if nmarshal>0 and npattern<1 then - -- First flight send to marshal stack. + -- Next flight in line to be send from marshal to pattern. local marshalflight=self.Qmarshal[1] --#AIRBOSS.Flightitem - -- Time flight is marshalling. + -- Time flight is marshaling. local Tmarshal=timer.getAbsTime()-marshalflight.time self:I(self.lid..string.format("Marshal time of group %s = %d seconds", marshalflight.groupname, Tmarshal)) -- Time (last) flight has entered landing pattern. - local Tpattern=999 + local Tpattern=9999 + local npunits=1 if npattern>0 then + + -- Last flight group send to pattern. local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.Flightitem + + -- Number of aircraft in this group. + local npunits=patternflight.nunits + + -- Get time in pattern. Tpattern=timer.getAbsTime()-patternflight.time - self:I(self.lid..string.format("Pattern time of group %s = %d seconds", patternflight.groupname, Tpattern)) + self:I(self.lid..string.format("Pattern time of group %s = %d seconds. # of units=%d.", patternflight.groupname, Tpattern, npunits)) end -- Min time in pattern before next aircraft is allowed. - local TpatternMin=120 + local TpatternMin if self.case==1 then - TpatternMin=45 + TpatternMin=45*npunits -- 45 seconds interval per plane! else - TpatternMin=120 + TpatternMin=120*npunits -- 120 seconds interval per plane! end -- Min time in marshal before send to landing pattern. local TmarshalMin=120 - -- Two minutes in pattern at leastand >45 sec interval between pattern flights. + -- Two minutes in pattern at least and >45 sec interval between pattern flights. if self:IsRecovering() and Tmarshal>TmarshalMin and Tpattern>TpatternMin then - self:_CollapseMarshalStack() + self:_CheckCollapseMarshalStack() end end @@ -1320,11 +1332,20 @@ function AIRBOSS:_ScanCarrierZone() end +--- Get onboard number of player or client. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #string Onboard number as string. +function AIRBOSS:_GetOnboardNumberPlayer(group) + return self:_GetOnboardNumbers(group, true) +end + --- Get onboard numbers of all units in a group. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. +-- @param #boolean playeronly If true, return the onboard number for player or client skill units. -- @return #table Table of onboard numbers. -function AIRBOSS:_GetOnboardNumbers(group) +function AIRBOSS:_GetOnboardNumbers(group, playeronly) --self:F({groupname=group:GetName}) -- Get group name. @@ -1344,12 +1365,17 @@ function AIRBOSS:_GetOnboardNumbers(group) local n=tostring(unit.onboard_num) local name=unit.name local skill=unit.skill - - -- Table entry. - numbers[name]=n - + -- Debug text. text=text..string.format("\n- unit %s: onboard #=%s skill=%s", name, n, skill) + + if playeronly and skill=="Client" or skill=="Player" then + -- There can be only one player in the group. so we skill everything else + return n + end + + -- Table entry. + numbers[name]=n end -- Debug info. @@ -1373,7 +1399,6 @@ function AIRBOSS:_CreateFlightGroup(group) flight.group=group flight.groupname=group:GetName() flight.nunits=#group:GetUnits() - flight.fuel=group:GetFuelMin() flight.time=timer.getAbsTime() flight.dist0=group:GetCoordinate():Get2DDistance(self:GetCoordinate()) flight.flag=USERFLAG:New(groupname) @@ -1381,6 +1406,7 @@ function AIRBOSS:_CreateFlightGroup(group) flight.ai=not human flight.actype=group:GetTypeName() flight.onboardnumbers=self:_GetOnboardNumbers(group) + flight.section={} if human then @@ -1388,11 +1414,11 @@ function AIRBOSS:_CreateFlightGroup(group) local playerData=self:_GetPlayerDataGroup(group) flight.player=playerData + -- Message to player. + MESSAGE:New(string.format("%s, your flight is registered within CCA.", playerData.name), 10, "MARSHAL"):ToClient(playerData.client) + else - - -- Send AI to holding pattern. - --self:_MarshalAI(flight) - + -- Nothing to do for AI. end -- Add to known flights inside CCA zone. @@ -1419,31 +1445,34 @@ end --- Orbit at a specified position at a specified alititude with a specified speed. -- @param #AIRBOSS self --- @param Wrapper.Group#GROUP group Group containing the player unit. -function AIRBOSS:_MarshalPlayer(group) - - -- Flight group name. - local groupname=group:GetName() +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #AIRBOSS.Flightitem flight Flight group. +function AIRBOSS:_MarshalPlayer(playerData, flight) -- Number of full marshal stacks. local nstacks=#self.Qmarshal - -- Get player data. - local playerData=self:_GetPlayerDataGroup(group) - - -- Get flight data. - local knownflight=self:_GetFlightFromGroupInQueue(group, self.flights) - -- Check if flight is known to the airboss already. - if playerData and knownflight then + if playerData and flight then + -- Add group to marshal stack. - self:_AddMarshallGroup(knownflight, nstacks+1) + self:_AddMarshallGroup(flight, nstacks+1) + -- Set step to holding. playerData.step=AIRBOSS.PatternStep.HOLDING + + -- Set values for all flights in section. + for _,_flight in pairs(flight.section) do + local flight=_flight --#AIRBOSS.Flightitem + flight.player.step=AIRBOSS.PatternStep.HOLDING + flight.flag:Set(nstacks+1) + end + else + -- Flight is not registered yet. - local text="You are not yet registered inside the CCA. Marshal request denied!" - self:_SendMessageToPlayer(text, 30, playerData) + local text="you are not yet registered inside the CCA. Marshal request denied!" + self:MessageToPlayer(playerData, text, "AIRBOSS") end end @@ -1529,6 +1558,11 @@ end -- @return Core.Point#COORDINATE Second holding position coordinate of racetrack pattern for CASE II/III recoveries. function AIRBOSS:_GetMarshalAltitude(stack) + -- Stack <= 0. + if stack<=0 then + return 0,nil,nil + end + -- Carrier position. local Carrier=self:GetCoordinate() local hdg=self.carrier:GetHeading() @@ -1558,13 +1592,34 @@ function AIRBOSS:_GetMarshalAltitude(stack) return altitude, p1, p2 end +--- Get number of groups and units in queue. +-- @param #AIRBOSS self +-- @param #table queue The queue. Can me all, marshal or pattern. +-- @return #number Number of aircraft. +-- @return #number Number of flight groups. +function AIRBOSS:_GetQueueInfo(queue) + + local ngroup=0 + local nunits=0 + + for _,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.Flightitem + + ngroup=ngroup+1 + nunits=nunits+flight.nunits + + end + + return nunits, ngroup +end + --- Add a flight group to the marshal stack. -- @param #AIRBOSS self -- @param #AIRBOSS.Flightitem flight Flight group. -- @param #number flagvalue Initial user flag value = stack number for holding. function AIRBOSS:_AddMarshallGroup(flight, flagvalue) - -- Set flag value. + -- Set flag value. This corresponds to the stack number which starts at 1. flight.flag:Set(flagvalue) -- Pressure. @@ -1578,6 +1633,7 @@ function AIRBOSS:_AddMarshallGroup(flight, flagvalue) local brc=self:_BaseRecoveryCourse() -- Marshal message. + -- TODO: Get charlie time estimate. local text=string.format("%s, Case %d, BRC is %03d, hold at %d. Expected Charlie Time XX.\n", boardnumber, self.case, brc, alt) text=text..string.format("Altimeter %.2f. Report see me.", P) MESSAGE:New(text, 30):ToAll() @@ -1586,6 +1642,30 @@ function AIRBOSS:_AddMarshallGroup(flight, flagvalue) table.insert(self.Qmarshal, flight) end +--- Check if marshal stack can be collapsed. +-- If next in line is an AI flight, this is done. If human player is next, we wait for "Commence" via F10 radio menu command. +-- @param #AIRBOSS self +function AIRBOSS:_CheckCollapseMarshalStack() + + -- First flight to enter the landing pattern. + local flight=self.Qmarshal[1] --#AIRBOSS.Flightitem + + -- TODO: better message. + local text=string.format("%s, you are cleared for Case %d recovery pattern!", flight.groupname, self.case) + + -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence. + if flight.ai then + -- Collapse stack and send AI to pattern. + self:_CollapseMarshalStack() + else + -- TODO only if skil is not TOPGUN + text=text..string.format("\nUse F10 radio menu \"Commence!\" command when you are ready!") + end + + -- TODO: Message to all players! + MESSAGE:New(text, 15, "MARSHAL"):ToAll() +end + --- Collapse marshal stack. -- @param #AIRBOSS self function AIRBOSS:_CollapseMarshalStack() @@ -1593,14 +1673,25 @@ function AIRBOSS:_CollapseMarshalStack() -- Decrease flag values of all flight groups in marshal stack. for _,_flight in pairs(self.Qmarshal) do local flight=_flight --#AIRBOSS.Flightitem + + -- Get current flag/stack value. local flagvalue=flight.flag:Get() + + -- Decrease by one. flight.flag:Set(flagvalue-1) + + -- Also decrease flag for section members of flight. + for _,_sec in pairs(flight.section) do + local sec=_sec --#AIRBOSS.Flightitem + sec.flag:Set(flagvalue-1) + end + end -- Number of marshal flight groups. local nmarshal=#self.Qmarshal - -- TODO: collapse marschal stack only from N to N-x. For example, when a group in the stack leaves (e.g. for refuelling). + -- TODO: collapse marshal stack only from N to N-x. For example, when a group in the stack leaves (e.g. for refueling). for i=nmarshal,1,-1 do local flight=self.Qmarshal[i] --#AIRBOSS.Flightitem --flight. @@ -1611,21 +1702,10 @@ function AIRBOSS:_CollapseMarshalStack() self:I(self.lid..string.format("New pattern flight %s.", flight.groupname)) - -- TODO: better message. - MESSAGE:New(string.format("Marshal, %s, you are cleared for Case I recovery pattern!", flight.groupname), 15):ToAll() - - -- Set player step. - if flight.ai==false then - local playerData=self:_GetPlayerDataGroup(flight.group) - - - playerData.step=AIRBOSS.PatternStep.COMMENCING - end - -- New time stamp for time in pattern. flight.time=timer.getAbsTime() - -- Add flight to pattern queue + -- Add flight to pattern queue. table.insert(self.Qpattern, flight) -- Remove flight from marshal queue. @@ -1962,11 +2042,14 @@ function AIRBOSS:OnEventLand(EventData) -- Landing distance to carrier position. local dist=coord:Get2DDistance(self:GetCoordinate()) + -- TODO: check if 360 degrees correctino is necessary! + local hdg=self.carrier:GetHeading()+self.carrierparam.rwyangle + -- Debug marks of wires. - local w1=self:GetCoordinate():Translate(self.carrierparam.wire1, 0):MarkToAll("Wire 1") - local w2=self:GetCoordinate():Translate(self.carrierparam.wire2, 0):MarkToAll("Wire 2") - local w3=self:GetCoordinate():Translate(self.carrierparam.wire3, 0):MarkToAll("Wire 3") - local w4=self:GetCoordinate():Translate(self.carrierparam.wire4, 0):MarkToAll("Wire 4") + local w1=self:GetCoordinate():Translate(self.carrierparam.wire1, hdg):MarkToAll("Wire 1") + local w2=self:GetCoordinate():Translate(self.carrierparam.wire2, hdg):MarkToAll("Wire 2") + local w3=self:GetCoordinate():Translate(self.carrierparam.wire3, hdg):MarkToAll("Wire 3") + local w4=self:GetCoordinate():Translate(self.carrierparam.wire4, hdg):MarkToAll("Wire 4") -- We did land. playerData.landed=true @@ -1998,8 +2081,7 @@ function AIRBOSS:OnEventLand(EventData) if self:_InQueue(self.Qpattern, EventData.IniGroup) then self:_RemoveQueue(self.Qpattern, EventData.IniGroup) end - - + end end @@ -2017,13 +2099,12 @@ function AIRBOSS:OnEventCrash(EventData) self:I(self.lid.."CRASH: group = "..tostring(EventData.IniGroupName)) self:I(self.lid.."CARSH: player = "..tostring(_playername)) - -- TODO: Update queues! - -- TODO: decrease number of units in group + -- TODO: Decrease number of units in group! if _unit and _playername then - self:I(self.lid.."Player %s crashed!",_playername) + self:I(self.lid..string.format("Player %s crashed!",_playername)) else - self:I(self.lid.."AI unit %s crashed!", EventData.IniUnitName) + self:I(self.lid..string.format("AI unit %s crashed!", EventData.IniUnitName)) end end @@ -2053,6 +2134,9 @@ function AIRBOSS:_NewPlayer(unitname) playerData.group = playerunit:GetGroup() playerData.callsign = playerData.unit:GetCallsign() playerData.client = CLIENT:FindByName(unitname, nil, true) + playerData.actype = playerunit:GetTypeName() + playerData.onboard = self:_GetOnboardNumberPlayer(playerData.group) + playerData.seclead = playername -- Number of passes done by player. playerData.passes=playerData.passes or 0 @@ -2065,9 +2149,6 @@ function AIRBOSS:_NewPlayer(unitname) -- Set difficulty level. playerData.difficulty=playerData.difficulty or AIRBOSS.Difficulty.NORMAL - - -- Player is in the big zone around the carrier. - --playerData.inbigzone=playerData.unit:IsInZone(self.zoneCCA) -- Init stuff for this round. playerData=self:_InitPlayer(playerData) @@ -2102,8 +2183,6 @@ function AIRBOSS:_InitPlayer(playerData) return playerData end -local _bla=true - --- Holding. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. @@ -2122,37 +2201,9 @@ function AIRBOSS:_Holding(playerData) -- Player altitude. local playeralt=unit:GetAltitude() - -- Create a holding zone depending on recovery case. - local zoneHolding --Core.Zone#ZONE - if self.case==1 then - -- CASE I + -- Get holding zone of player. + local zoneHolding=self:_GetHoldingZone(playerData) - -- Zone 2.5 NM port of carrier with a radius of 3 NM (holding pattern should be < 5 NM). - zoneHolding=ZONE_UNIT:New("CASE I Holding Zone", self.carrier, UTILS.NMToMeters(3), {dx=0, dy=-UTILS.NMToMeters(2.5), relative_to_unit=true}) - - else - -- CASE II/II - - local hdg=self.carrier:GetHeading() - - -- Create an array of a square! - local p={} - p[1]=c1:Translate(UTILS.NMToMeters(1), hdg+90):GetVec2() --c1 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. - p[2]=c2:Translate(UTILS.NMToMeters(1), hdg+90):GetVec2() --c2 is 10 NM further behind. Also translated 1 NM starboard. - p[3]=c2:Translate(UTILS.NMToMeters(7), hdg-90):GetVec2() --p3 6 NM port of carrier. - p[4]=c1:Translate(UTILS.NMToMeters(7), hdg-90):GetVec2() --p4 6 NM port of carrier. - - -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. - -- So stay 0-5 NM (+1 NM error margin) port of carrier. - zoneHolding=ZONE_POLYGON_BASE:New("CASE II/III Holding Zone", p) - end - - - if _bla then - zoneHolding:SmokeZone(SMOKECOLOR.Green) - _bla=false - end - -- Check if player is in holding zone. local inholdingzone=unit:IsInZone(zoneHolding) @@ -2216,11 +2267,63 @@ function AIRBOSS:_Holding(playerData) end - if text~="" then - self:_SendMessageToPlayer(text, 5, playerData, false, "AIRBOSS") - end + -- Send message. + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 5) +end -end +--- Get holding zone of player. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return Core.Zone#ZONE Holding zone. +function AIRBOSS:_GetHoldingZone(playerData) + + -- Player unit and flight. + local unit=playerData.unit + local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) + + -- Create a holding zone depending on recovery case. + local zoneHolding=nil --Core.Zone#ZONE + + if flight then + + -- Current stack. + local stack=flight.flag:Get() + + -- Stack is <= 0 ==> no marshal zone. + if stack<=0 then + return nil + end + + -- Pattern alitude. + local patternalt, c1, c2=self:_GetMarshalAltitude(stack) + + if self.case==1 then + -- CASE I + + -- Zone 2.5 NM port of carrier with a radius of 3 NM (holding pattern should be < 5 NM). + zoneHolding=ZONE_UNIT:New("CASE I Holding Zone", self.carrier, UTILS.NMToMeters(3), {dx=0, dy=-UTILS.NMToMeters(2.5), relative_to_unit=true}) + + else + -- CASE II/II + + -- TODO: Include 15 or 30 degrees offset. + local hdg=self.carrier:GetHeading() + + -- Create an array of a square! + local p={} + p[1]=c1:Translate(UTILS.NMToMeters(1), hdg+90):GetVec2() --c1 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. + p[2]=c2:Translate(UTILS.NMToMeters(1), hdg+90):GetVec2() --c2 is 10 NM further behind. Also translated 1 NM starboard. + p[3]=c2:Translate(UTILS.NMToMeters(7), hdg-90):GetVec2() --p3 6 NM port of carrier. + p[4]=c1:Translate(UTILS.NMToMeters(7), hdg-90):GetVec2() --p4 6 NM port of carrier. + + -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. + -- So stay 0-5 NM (+1 NM error margin) port of carrier. + zoneHolding=ZONE_POLYGON_BASE:New("CASE II/III Holding Zone", p) + end + end + + return zoneHolding +end --- Commence approach. -- @param #AIRBOSS self @@ -3017,8 +3120,8 @@ function AIRBOSS:_DetailedPlayerStatus(playerData) text=text..string.format("AoA=%.1f | |V|=%.1f knots\n", aoa, UTILS.MpsToKnots(vabs)) text=text..string.format("Vx=%.1f Vy=%.1f Vz=%.1f m/s\n", velo.x, velo.y, velo.z) text=text..string.format("Pitch=%.1f° | Roll=%.1f° | Yaw=%.1f°\n", pitch, roll, yaw) - text=text..string.format("Climb Angle=%.1f°\n | Rate=%d ft/min\n", unit:GetClimbAngle(), velo.y*196.85) - text=text..string.format("R=%d NM | X=%d Z=%d m\n", UTILS.MetersToNM(rho), dx, dz) + text=text..string.format("Climb Angle=%.1f° | Rate=%d ft/min\n", unit:GetClimbAngle(), velo.y*196.85) + text=text..string.format("R=%.1f NM | X=%d Z=%d m\n", UTILS.MetersToNM(rho), dx, dz) text=text..string.format("Phi=%.1f° | Rel=%.1f°", phi, relhead) -- If in the groove, provide line up and glide slope error. if playerData.step==AIRBOSS.PatternStep.GROOVE_XX or @@ -3831,6 +3934,8 @@ function AIRBOSS:_Debrief(playerData) text=text..string.format("%s\n", comment) end + local text="Your detailed debriefing can now be seen in F10 radio menu." + -- Send debrief message to player self:_SendMessageToPlayer(text, 30, playerData, true, "Paddles") @@ -3908,7 +4013,7 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay) -- Broadcast message. radio:Broadcast(true) - -- Subtitle. + -- "Subtitle". for _,_player in pairs(self.players) do local playerData=_player --#AIRBOSS.PlayerData self:_SendMessageToPlayer(call.subtitle, call.duration, playerData) @@ -3940,7 +4045,7 @@ end -- @param #number delay Delay in seconds, before the message is send. function AIRBOSS:_SendMessageToPlayer(message, duration, playerData, clear, sender, delay) - if playerData and message then + if playerData and message and message~="" then -- Format message. local text=string.format("%s, %s", playerData.callsign, message) @@ -3958,6 +4063,46 @@ function AIRBOSS:_SendMessageToPlayer(message, duration, playerData, clear, send end +--- Send text message to player client. +-- Message format will be " MESSAGE". +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string message The message to send. +-- @param #string sender The person who sends the message. +-- @param #string receiver The person who receives the message. +-- @param #number duration Display message duration. Default 5 sec. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is send. +function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay) + + if playerData and message and message~="" then + + -- Default duration. + duration=duration or 10 + + -- Format message. + local text + if receiver and receiver=="" then + text=string.format("%s", message) + else + receiver=receiver or playerData.onboard + text=string.format("%s, %s", receiver, message) + end + self:I(self.lid..text) + + if delay and delay>0 then + SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) + else + if playerData.client then + MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) + end + end + + end + +end + + --- Checks if a group has a human player. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. @@ -4069,7 +4214,7 @@ function AIRBOSS:GetCoordinate() end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Menu Functions +-- RADIO MENU Functions ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Add menu commands for player. @@ -4120,22 +4265,24 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "My Grades", _statsPath, self._DisplayPlayerGrades, self, _unitName) --missionCommands.addCommandForGroup(_gid, "(Clear ALL Results)", _statsPath, self._ResetRangeStats, self, _unitName) - -- F10/Airboss//Difficulty + -- F10/Airboss//Skill Level missionCommands.addCommandForGroup(_gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) missionCommands.addCommandForGroup(_gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) - - + -- F10/Airboss//Kneeboard - missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _kneeboardPath, self._AttitudeMonitor, self, playername) - missionCommands.addCommandForGroup(_gid, "Weather Report", _kneeboardPath, self._DisplayCarrierWeather, self, _unitName) missionCommands.addCommandForGroup(_gid, "Carrier Info", _kneeboardPath, self._DisplayCarrierInfo, self, _unitName) - - -- F10/Airboss// - missionCommands.addCommandForGroup(_gid, "Request Marshal?", _rootPath, self._RequestMarshal, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Weather Report", _kneeboardPath, self._DisplayCarrierWeather, self, _unitName) + missionCommands.addCommandForGroup(_gid, "My Status", _kneeboardPath, self._DisplayPlayerStatus, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _kneeboardPath, self._AttitudeMonitor, self, playername) + missionCommands.addCommandForGroup(_gid, "Smoke Marshal Zone", _kneeboardPath, self._SmokeMarshalZone, self, _unitName) + + -- F10/Airboss// + missionCommands.addCommandForGroup(_gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) missionCommands.addCommandForGroup(_gid, "Commencing!", _rootPath, self._RequestStraightIn, self, _unitName) - - --TODO: request refulling if recovery tanker set! make refuelling queue. add refuelling step. + missionCommands.addCommandForGroup(_gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Set Section", _rootPath, self._SetSection, self, _unitName) + --TODO: request refueling if recovery tanker set! make refuelling queue. add refuelling step. end else @@ -4147,6 +4294,166 @@ function AIRBOSS:_AddF10Commands(_unitName) end +--- Player requests refueling. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_RequestRefueling(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + local text="Player requested refueling." + MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) + + end + end +end + +--- Smoke current marshal zone of player. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_SmokeMarshalZone(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Get current holding zone. + local zone=self:_GetHoldingZone(playerData) + + local text="No marshal zone to smoke!" + if zone then + text="Smoking marshal zone with GREEN smoke." + zone:SmokeZone(SMOKECOLOR.Green) + end + MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) + end + end + +end + +--- Display player status. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_DisplayPlayerStatus(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Player data. + local text=string.format("Status of player %s (%s)\n", playerData.name, playerData.callsign) + text=text..string.format("--------------------------------------\n") + text=text..string.format("Current step: %s\n", playerData.step) + text=text..string.format("Skil level: %s\n", playerData.difficulty) + text=text..string.format("Aircraft: %s\n", playerData.actype) + text=text..string.format("Board number: %s\n", playerData.onboard) + text=text..string.format("Fuel: %.1f %%\n", playerData.unit:GetFuel()*100) + text=text..string.format("Group name: %s\n", playerData.group:GetName()) + text=text..string.format("# units: %s\n", #playerData.group:GetUnits()) + text=text..string.format("Section Lead: %s\n", tostring(playerData.seclead)) + + -- Flight data (if available). + local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) + if flight then + local stack=flight.flag:Get() + local stackalt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) + text=text..string.format("Aircraft: %s\n", flight.actype) + text=text..string.format("Flag/stack: %d\n", stack) + text=text..string.format("Stack alt: %d ft\n", stackalt) + text=text..string.format("# units: %s\n", flight.nunits) + text=text..string.format("# section: %s", #flight.section) + for _,_sec in pairs(flight.section) do + local sec=_sec --#AIRBOSS.Flightitem + text=text..string.format("\n- %s", sec.player.name) + end + else + text=text..string.format("Your flight is not registered in CCA.") + end + + if playerData.step==AIRBOSS.PatternStep.INITIAL then + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) + local flydist=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) + local brc=self:_BaseRecoveryCourse() + text=text..string.format("Fly heading %03d° for %d NM and turn to BRC %03d°.", flyhdg, flydist, brc) + end + + MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) + end + end + +end + +--- Set all flights within 200 meters to be part of my section. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_SetSection(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Coordinate of flight lead. + local mycoord=_unit:GetCoordinate() + + -- Flight group. + local myflight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) + + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.Flightitem + + -- Only human flight groups excluding myself. + if flight.ai==false and flight.player and flight.groupname~=myflight.groupname then + + -- Distance to other group. + local distance=flight.group:GetCoordinate():Get2DDistance(mycoord) + + if distance<200 then + table.insert(myflight.section, flight) + end + + end + end + + local text + if #myflight.section>0 then + text=string.format("Registered flight section") + text=text..string.format("- %s (lead)", myflight.player.name) + for _,_flight in paris(myflight.section) do + local flight=_flight --#AIRBOSS.Flightitem + text=text..string.format("- %s", flight.player.name) + end + else + text="No other human flights found within radius of 200 meter radius!" + end + MESSAGE:New(text, 10, "MARSHALL"):ToAll() + + end + end + +end + --- Request straight in approach. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. @@ -4161,9 +4468,49 @@ function AIRBOSS:_RequestStraightIn(_unitName) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then - -- TODO: check if landing pattern is full. If so, display message "AIRBOSS: "Pattern is full." and deny step! - -- TODO: check if in marshal stack and flag is 0. If not, give message "AIRBOSS: It's not your turn yet!" and deny step! - playerData.step=AIRBOSS.PatternStep.COMMENCING + + -- Get flight group. + local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) + + local text + if flight then + + -- Get stack value. + local stack=flight.flag:Get() + + if stack>1 then + -- We are in a higher stack. + text="Negative ghostrider, it's not your turn yet!" + else + + -- Number of aircraft currently in pattern. + local npattern=self:_GetQueueInfo(self.Qpattern) + + -- TODO: set nmax for pattern. Should be ~6 but let's make this 4. + if npattern>0 then + -- Patern is full! + text=string.format("Negative ghostrider, pattern is full! There are %d aircraft currently in pattern.", npattern) + else + -- Positive response. + text="You are cleared for pattern. Proceed to initial." + + -- Set player step. + playerData.step=AIRBOSS.PatternStep.COMMENCING + + -- Collaps marshal stack. + self:_CollapseMarshalStack() + end + + end + + else + -- This flight is not yet registered! + text="Negative ghostrider, you are not yet registered inside the CCA yet!" + -- TODO: fly 10 km towards the carrier advice for skill "Flight Student" + end + + -- Send message. + self:MessageToPlayer(playerData, text, "AIRBOSS", "", 5) end end end @@ -4182,11 +4529,60 @@ function AIRBOSS:_RequestMarshal(_unitName) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then - self:_MarshalPlayer(playerData.group) + + -- Get flight group. + local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) + + if flight then + self:_MarshalPlayer(playerData, flight) + else + -- Flight group does not exist yet. + local text=string.format("%s, you are not registered inside CCA yet. Marshal request denied!", playerData.name) + MESSAGE:New(text, 10, "MARSHAL"):ToClient(playerData.client) + end end end end +--- Display last debriefing. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayPlayerGrades(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Debriefing text. + local text=string.format("Debriefing:") + + -- Check if data is present. + if #playerData.debrief>0 then + text=text..string.format("\n================================\n") + for _,_data in pairs(playerData.debrief) do + local step=_data.step + local comment=_data.hint + text=text..string.format("* %s:\n",step) + text=text..string.format("%s\n", comment) + end + else + text=text.." Nothing to show yet." + end + + -- Send debrief message to player + self:MessageToPlayer(playerData, text, nil , "", 30, true) + + end + end +end + + --- Display top 10 player scores. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. @@ -4311,6 +4707,7 @@ function AIRBOSS:_SetDifficulty(playername, difficulty) playerData.difficulty=difficulty local text=string.format("Your difficulty level is now: %s.", difficulty) self:_SendMessageToPlayer(text, 5, playerData) + self:MessageToPlayer(playerData, text, nil, "", 5) else self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) end @@ -4354,19 +4751,19 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) local text=string.format("%s info:\n", self.alias) text=text..string.format("Case %d Recovery\n", self.case) text=text..string.format("BRC %03d°\n", self:_BaseRecoveryCourse()) - text=text..string.format("FB %03d°\n", self:_FinalBearing()) + text=text..string.format("FB %03d°\n", self:_FinalBearing()) text=text..string.format("Speed %d kts\n", carrierspeed) - text=text..string.format("Airboss radio %.3f MHz AM\n", self.Carrierfreq) --TODO: add modulation - text=text..string.format("LSO radio %.3f MHz AM\n", self.LSOfreq) + text=text..string.format("Airboss radio %.3f MHz\n", self.Carrierfreq) --TODO: add modulation + text=text..string.format("LSO radio %.3f MHz\n", self.LSOfreq) text=text..string.format("TACAN Channel %s\n", tacan) text=text..string.format("ICLS Channel %s\n", icls) text=text..string.format("# A/C total %d\n", #self.flights) text=text..string.format("# A/C holding %d\n", #self.Qmarshal) - text=text..string.format("# A/C pattern %d", #self.Qpattern) + text=text..string.format("# A/C pattern %d", #self.Qpattern) self:T2(self.lid..text) -- Send message. - self:_SendMessageToPlayer(text, 20, playerData, true) + self:MessageToPlayer(playerData, text, nil, "", 20, true) else self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) @@ -4428,7 +4825,7 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) self:T2(self.lid..text) -- Send message to player group. - self:_SendMessageToPlayer(text, 30, self.players[playername]) + self:MessageToPlayer(self.players[playername], text, nil, "", 30, true) else self:E(self.lid..string.format("ERROR! Could not find player unit in CarrierWeather! Unit name = %s", _unitname)) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 58518093d..4e4867c60 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -25,6 +25,9 @@ -- @field #string tankergroupname Name of the late activated tanker template group. -- @field Wrapper.Group#GROUP tanker Tanker group. -- @field Wrapper.Airbase#AIRBASE airbase The home airbase object of the tanker. Normally the aircraft carrier. +-- @field Core.Radio#BEACON beacon Tanker TACAN beacon. +-- @field #number TACANchannel TACAN channel. Default 1. +-- @field #string TACANmode TACAN mode, i.e. "X" or "Y". Default "Y". -- @field #number speed Tanker speed when flying pattern. -- @field #number altitude Tanker orbit pattern altitude. -- @field #number distStern Race-track distance astern. @@ -56,6 +59,9 @@ RECOVERYTANKER = { tankergroupname = nil, tanker = nil, airbase = nil, + beacon = nil, + TACANchannel = nil, + TACANmode = nil, altitude = nil, speed = nil, distStern = nil, @@ -72,7 +78,7 @@ RECOVERYTANKER = { --- Class version. -- @field #string version -RECOVERYTANKER.version="0.9.2" +RECOVERYTANKER.version="0.9.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -80,6 +86,7 @@ RECOVERYTANKER.version="0.9.2" -- TODO: Smarter pattern update function. E.g. (small) zone around carrier. Only update position when carrier leaves zone or changes heading? -- TODO: Write documenation. +-- DONE: Set AA TACAN. -- DONE: Add refueling event/state. -- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. @@ -118,6 +125,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetTakeoffAir() self:SetLowFuelThreshold() self:SetRespawnOnOff() + self:SetTACAN() ----------------------- --- FSM Transitions --- @@ -334,6 +342,17 @@ function RECOVERYTANKER:SetUseUncontrolledAircraft() return self end +--- Set TACAN channel of tanker. +-- @param #RECOVERYTANKER self +-- @param #number channel TACAN channel. Default 1. +-- @param #string mode TACAN mode, i.e. "X" or "Y". Default "Y". +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTACAN(channel, mode) + self.TACANchannel=channel or 1 + self.TACANmode=mode or "Y" + return self +end + --- Check if tanker is returning to base. -- @param #RECOVERYTANKER self -- @return #boolean If true, tanker is returning to base. @@ -400,9 +419,12 @@ function RECOVERYTANKER:onafterStart(From, Event, To) self.tanker=GROUP:FindByName(self.tankergroupname) if self.tanker:IsAlive() then + -- Start uncontrolled group. self.tanker:StartUncontrolled() + else + -- No group by that name! self:E(string.format("ERROR: No uncontrolled (alive) tanker group with name %s could be found!", self.tankergroupname)) return end @@ -417,7 +439,11 @@ function RECOVERYTANKER:onafterStart(From, Event, To) -- Initialize route. self:_InitRoute(30, 10, 1) - end + end + + -- Create tanker beacon. + self.beacon=BEACON:New(self.tanker:GetUnit(1)) + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, "TKR", true) -- Init status check. self:__Status(10) @@ -478,7 +504,7 @@ end function RECOVERYTANKER:onbeforeRTB(From, Event, To) -- Check if spawn in air is activated. - if self.takeoff==SPAWN.Takeoff.Air then + if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then -- Check that respawn should happen. if self.respawn then @@ -491,6 +517,10 @@ function RECOVERYTANKER:onbeforeRTB(From, Event, To) self.tanker:InitHeading(self.tanker:GetHeading()) self.tanker=self.tanker:Respawn(nil, true) + -- Create tanker beacon. + self.beacon=BEACON:New(self.tanker:GetUnit(1)) + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, "TKR", true) + -- Update Pattern in 2 seconds. Need to give a bit time so that the respawned group is in the game. SCHEDULER:New(nil, self._PatternUpdate, {self}, 2) @@ -562,6 +592,10 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) -- Respawn tanker. self.tanker=group:RespawnAtCurrentAirbase() + + -- Create tanker beacon. + self.beacon=BEACON:New(self.tanker:GetUnit(1)) + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, "TKR", true) -- Initial route. self:_InitRoute() diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 6e8a83d1d..0c767d3eb 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -34,6 +34,9 @@ -- @field #number offsetX Offset in meters to carrier in longitudinal direction. -- @field #number offsetZ Offset in meters to carrier in latitudinal direction. -- @field Core.Zone#ZONE_RADIUS rescuezone Zone around the carrier in which helo will rescue crashed or ejected units. +-- @field #boolean respawn If true, helo be respawned (default). If false, no respawning will happen. +-- @field #boolean respawninair If true, helo will always be respawned in air. This has no impact on the initial spawn setting. +-- @field #boolean uncontrolledac If true, use and uncontrolled helo group already present in the mission. -- @extends Core.Fsm#FSM --- Rescue Helo @@ -48,25 +51,28 @@ -- -- @field #RESCUEHELO RESCUEHELO = { - ClassName = "RESCUEHELO", - carrier = nil, - carriertype = nil, - helogroupname = nil, - helo = nil, - airbase = nil, - takeoff = nil, - followset = nil, - formation = nil, - lowfuel = nil, - altitude = nil, - offsetX = nil, - offsetZ = nil, - rescuezone = nil, + ClassName = "RESCUEHELO", + carrier = nil, + carriertype = nil, + helogroupname = nil, + helo = nil, + airbase = nil, + takeoff = nil, + followset = nil, + formation = nil, + lowfuel = nil, + altitude = nil, + offsetX = nil, + offsetZ = nil, + rescuezone = nil, + respawn = nil, + respawninair = nil, + uncontrolledac = nil, } --- Class version. -- @field #string version -RESCUEHELO.version="0.9.2" +RESCUEHELO.version="0.9.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -273,6 +279,56 @@ function RESCUEHELO:SetOffsetZ(distance) end +--- Enable respawning of helo. Note that this is the default behaviour. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOn() + self.respawn=true + return self +end + +--- Disable respawning of helo. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOff() + self.respawn=false + return self +end + +--- Set whether helo shall be respawned or not. +-- @param #RESCUEHELO self +-- @param #boolean switch If true (or nil), helo will be respawned. If false, helo will not be respawned. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOnOff(switch) + if switch==nil or switch==true then + self.respawn=true + else + self.respawn=false + end + return self +end + +--- Helo will be respawned in air, even it was initially spawned on the carrier. +-- So only the first spawn will be on the carrier while all subsequent spawns will happen in air. +-- This allows for undisrupted operations and less problems on the carrier deck. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnInAir() + self.respawninair=true + return self +end + +--- Use an uncontrolled aircraft already present in the mission rather than spawning a new helo as initial rescue helo. +-- This can be useful when interfaced with, e.g., a warehouse. +-- The group name is the one specified in the @{#RESCUEHELO.New} function. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetUseUncontrolledAircraft() + self.uncontrolledac=true + return self +end + + --- Check if helo is returning to base. -- @param #RESCUEHELO self -- @return #boolean If true, helo is returning to base. @@ -312,9 +368,9 @@ function RESCUEHELO:OnEventLand(EventData) -- Respawn the Helo. self:I(string.format("Respawning rescue helo group %s at home base.", groupname)) - if self.takeoff==SPAWN.Takeoff.Air then + if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then - self:E("ERROR: Rescue helo %s landed. This should not happen for Takeoff=Air!", groupname) + self:E("ERROR: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true!", groupname) else @@ -424,16 +480,41 @@ function RESCUEHELO:onafterStart(From, Event, To) delay=1 else - - -- Spawn at airbase. - self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) + + -- Check if an uncontrolled helo group was requested. + if self.useuncontrolled then - if self.takeoff==SPAWN.Takeoff.Runway then - delay=5 - elseif self.takeoff==SPAWN.Takeoff.Hot then - delay=30 - elseif self.takeoff==SPAWN.Takeoff.Cold then - delay=60 + -- Use an uncontrolled aircraft group. + self.helo=GROUP:FindByName(self.helogroupname) + + if self.helo:IsAlive() then + + -- Start uncontrolled group. + self.helo:StartUncontrolled() + + -- Delay before formation is started. + delay=60 + + else + -- No group of that name! + self:E(string.format("ERROR: No uncontrolled (alive) rescue helo group with name %s could be found!", self.helogroupname)) + return + end + + else + + -- Spawn at airbase. + self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) + + -- Delay before formation is started. + if self.takeoff==SPAWN.Takeoff.Runway then + delay=5 + elseif self.takeoff==SPAWN.Takeoff.Hot then + delay=30 + elseif self.takeoff==SPAWN.Takeoff.Cold then + delay=60 + end + end end @@ -454,9 +535,6 @@ function RESCUEHELO:onafterStart(From, Event, To) -- Start formation FSM. self.formation:__Start(delay) - -- Start uncontrolled helo. - --HeloSpawn:StartUncontrolled(120) - -- Init status check self:__Status(1) @@ -479,7 +557,7 @@ function RESCUEHELO:onafterStatus(From, Event, To) local text=string.format("Rescue Helo %s: state=%s fuel=%.1f", self.helo:GetName(), self:GetState(), fuel) self:I(text) - -- If fuel < threshold ==> send helo to home base! + -- If fuel < threshold ==> send helo to home base! if fuel Date: Wed, 21 Nov 2018 16:01:10 +0100 Subject: [PATCH 051/147] AIRBOSS v0.3.1w --- Moose Development/Moose/Ops/Airboss.lua | 463 +++++++++++++++--------- 1 file changed, 299 insertions(+), 164 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index f61d9442f..c1642a214 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -58,11 +58,11 @@ -- @field #AIRBOSS.Checkpoint Wake Right behind the carrier. -- @field #AIRBOSS.Checkpoint Groove In the groove checkpoint. -- @field #AIRBOSS.Checkpoint Trap Landing checkpoint. --- @field #AIRBOSS.Checkpoint C3Descent4k Case III descent at 4000 ft/min right after leaving holding pattern. --- @field #AIRBOSS.Checkpoint C3Descent2k Case III descent at 2000 ft/min at 5000 ft plattform. --- @field #AIRBOSS.Checkpoint C3DirtyUp Case III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. +-- @field #AIRBOSS.Checkpoint C3Descent4k Case II/III descent at 4000 ft/min right after leaving holding pattern. +-- @field #AIRBOSS.Checkpoint C3Descent2k Case II/III descent at 2000 ft/min at 5000 ft plattform. +-- @field #AIRBOSS.Checkpoint C3DirtyUp Case II/III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. -- @field #AIRBOSS.Checkpoint C3BullsEye Case III intercept glideslope and follow ICLS "bullseye". --- @field #number case Recovery case I or III in progress. +-- @field #number case Recovery case I, II or III in progress. -- @field #table flights List of all flights in the CCA. -- @field #table Qmarshal Queue of marshalling aircraft groups. -- @field #table Qpattern Queue of aircraft groups in the landing pattern. @@ -122,6 +122,7 @@ AIRBOSS = { flights = {}, Qpattern = {}, Qmarshal = {}, + Nmaxpattern = nil, rescuehelo = nil, tanker = nil, warehouse = nil, @@ -132,15 +133,18 @@ AIRBOSS = { -- @type AIRBOSS.AircraftPlayer -- @field #string AV8B AV-8B Night Harrier. -- @field #string HORNET F/A-18C Lot 20 Hornet. +-- @field #string A4EC Community A-4E-C mod. AIRBOSS.AircraftPlayer={ AV8B="AV8BNA", HORNET="FA-18C_hornet", + A4EC="A-4E-C", --TODO: Correct string? } --- Aircraft types capable of landing on carrier (human+AI). -- @type AIRBOSS.AircraftCarrier -- @field #string AV8B AV-8B Night Harrier. -- @field #string HORNET F/A-18C Lot 20 Hornet. +-- @field #string A4EC Community A-4E-C mod. -- @field #string S3B Lockheed S-3B Viking. -- @field #string S3BTANKER Lockheed S-3B Viking tanker. -- @field #string E2D Grumman E-2D Hawkeye AWACS. @@ -149,15 +153,14 @@ AIRBOSS.AircraftPlayer={ AIRBOSS.AircraftCarrier={ AV8B="AV8BNA", HORNET="FA-18C_hornet", + A4EC="A-4E-C", --TODO: Correct string? S3B="S-3B", S3BTANKER="S-3B Tanker", E2D="E-2C", FA18C="F/A-18C", F14A="F-14A", - --TODO: Add A4-E-C } - --- Carrier types. -- @type AIRBOSS.CarrierType -- @field #string STENNIS USS John C. Stennis (CVN-74) @@ -181,6 +184,13 @@ AIRBOSS.CarrierType={ -- @field #number wire3 Distance in meters from carrier position to third wire. -- @field #number wire4 Distance in meters from carrier position to fourth wire. +--- Aircraft Parameters. +-- @type AIRBOSS.AircraftParameters +-- @field #number AoA Onspeed Angle of Attack. +-- @field #number Dboat Ideal distance to the carrier. +-- @field #number + + --- Pattern steps. -- @type AIRBOSS.PatternStep AIRBOSS.PatternStep={ @@ -188,8 +198,8 @@ AIRBOSS.PatternStep={ COMMENCING="Commencing", HOLDING="Holding", DESCENT4K="Descent 4000 ft/min", - DESCENT2K="Descent 2000 ft/min", - DIRTYUP="Leven and Dirty Up", + DESCENT2K="Platform", + DIRTYUP="Level out and Dirty Up", BULLSEYE="Follow Bullseye", INITIAL="Initial", UPWIND="Upwind", @@ -416,6 +426,7 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.PlayerData player Player data for human pilots. -- @field #string actype Aircraft type name. -- @field #table onboardnumbers Onboard numbers of aircraft in the group. +-- @field #number case Recovery case of flight. -- @field #table section Other human flight groups belonging to this flight. This flight is the lead. --- Main radio menu. @@ -435,12 +446,12 @@ AIRBOSS.version="0.3.1" -- TODO: Add radio transmission queue for LSO and airboss. -- TODO: Get correct wire when trapped. -- TODO: Add radio check (LSO, AIRBOSS) to F10 radio menu. --- TODO: Monitor holding of players/AI in zoneHolding. +-- DONE: Monitor holding of players/AI in zoneHolding. -- TODO: Right pattern step after bolter/wo/patternWO? -- TODO: Handle crash event. Delete A/C from queue, send rescue helo, stop carrier? -- TODO: Add aircraft numbers in queue to carrier info F10 radio output. --- TODO: Transmission via radio. --- TODO: Get board numbers. +-- DONE: Transmission via radio. +-- DONE: Get board numbers. -- TODO: Get fuel state in pounds. -- TODO: Add user functions. -- TODO: Generalize parameters for other carriers. @@ -609,7 +620,7 @@ function AIRBOSS:New(carriername, alias) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- User functions +-- USER API Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Set carrier controlled area (CCA). @@ -846,7 +857,7 @@ function AIRBOSS:onafterStatus(From, Event, To) if time-self.Tqueue>30 then local text=string.format("Status %s.", self:GetState()) - self:I(self.lid..text) + self:I(self.lid..text) -- Scan carrier zone for new aircraft. self:_ScanCarrierZone() @@ -994,10 +1005,6 @@ function AIRBOSS:_InitStennis() self.C3Descent4k.LimitXmax=-UTILS.NMToMeters(20) --TODO: better rho dist. decrease descent 20 2000 ft/min at 5000 ft alt and user rad alt. self.C3Descent4k.LimitZmin=nil self.C3Descent4k.LimitZmax=nil - -- TODO: alt, AoA are more aircraft functions rather than carrier - self.C3Descent4k.Altitude=nil --UTILS.FeetToMeters(5000) - self.C3Descent4k.AoA=nil - self.C3Descent4k.Distance=nil -- 2k descent from 5k platform to 1200 dirty up level flight. self.C3Descent2k.name="2k Descent" @@ -1009,9 +1016,6 @@ function AIRBOSS:_InitStennis() self.C3Descent2k.LimitXmax=-UTILS.NMToMeters(12) --TODO: better rho dist! now switch to dirty up level flight 12 NM. self.C3Descent2k.LimitZmin=nil self.C3Descent2k.LimitZmax=nil - self.C3Descent2k.Altitude=UTILS.FeetToMeters(5000) - self.C3Descent2k.AoA=nil - self.C3Descent2k.Distance=-UTILS.NMToMeters(20) -- Level out at 1200 ft and dirty up. self.C3DirtyUp.name="Dirty Up" @@ -1023,23 +1027,17 @@ function AIRBOSS:_InitStennis() self.C3DirtyUp.LimitXmax=-UTILS.NMToMeters(3) --TODO: better rho dist! Intercept glideslope and follow bullseye. self.C3DirtyUp.LimitZmin=nil self.C3DirtyUp.LimitZmax=nil - self.C3DirtyUp.Altitude=UTILS.FeetToMeters(1200) - self.C3DirtyUp.AoA=nil - self.C3DirtyUp.Distance=-UTILS.NMToMeters(12) -- Intercept glide slope and follow bullseye. - self.C3DirtyUp.name="Bullseye" - self.C3DirtyUp.Xmin=-UTILS.NMToMeters(4) - self.C3DirtyUp.Xmax=nil - self.C3DirtyUp.Zmin=-UTILS.NMToMeters(30) - self.C3DirtyUp.Zmax= UTILS.NMToMeters(30) - self.C3DirtyUp.LimitXmin=nil - self.C3DirtyUp.LimitXmax=-UTILS.NMToMeters(1) --TODO: better rho dist! Call the ball. - self.C3DirtyUp.LimitZmin=nil + self.Bullseye.name="Bullseye" + self.Bullseye.Xmin=-UTILS.NMToMeters(4) + self.Bullseye.Xmax=nil + self.Bullseye.Zmin=-UTILS.NMToMeters(30) + self.Bullseye.Zmax= UTILS.NMToMeters(30) + self.Bullseye.LimitXmin=nil + self.Bullseye.LimitXmax=-UTILS.NMToMeters(1) --TODO: better rho dist! Call the ball. + self.Bullseye.LimitZmin=nil self.C3DirtyUp.LimitZmax=nil - self.C3DirtyUp.Altitude=UTILS.FeetToMeters(1200) - self.C3DirtyUp.AoA=nil - self.C3DirtyUp.Distance=-UTILS.NMToMeters(3) -- Upwind leg self.Upwind.name="Upwind" @@ -1051,9 +1049,6 @@ function AIRBOSS:_InitStennis() 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" @@ -1065,9 +1060,6 @@ function AIRBOSS:_InitStennis() 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" @@ -1079,9 +1071,6 @@ function AIRBOSS:_InitStennis() 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" @@ -1093,9 +1082,6 @@ function AIRBOSS:_InitStennis() 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" @@ -1107,9 +1093,6 @@ function AIRBOSS:_InitStennis() 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" @@ -1121,9 +1104,6 @@ function AIRBOSS:_InitStennis() 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" @@ -1135,9 +1115,6 @@ function AIRBOSS:_InitStennis() 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" @@ -1149,9 +1126,130 @@ function AIRBOSS:_InitStennis() 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 + +--- Get optimal aircraft flight parameters at checkpoint. +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #string step Pattern step. +-- @return #number Altitude in meters or nil. +-- @return #number Angle of Attack or nil. +-- @return #number Distance to carrier in meters or nil. +-- @return #number Speed in m/s or nil. +function AIRBOSS:_GetAircraftParameters(playerData, step) + + step=step or playerData.step + local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET + local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC + + local dist + local alt + local aoa + local speed + + if step==AIRBOSS.PatternStep.DESCENT4K then + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.DESCENT2K then + + alt=UTILS.FeetToMeters(5000) + + dist=UTILS.NMToMeters(20) + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.DIRTYUP then + + alt=UTILS.FeetToMeters(1200) + + dist=UTILS.NMToMeters(12) + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.BULLSEYE then + + alt=UTILS.FeetToMeters(1200) + + dist=-UTILS.NMToMeters(3) + + elseif step==AIRBOSS.PatternStep.INITIAL then + + if hornet then + alt=UTILS.FeetToMeters(800) + speed=UTILS.KnotsToMps(350) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + speed=UTILS.KnotsToMps(250) + end + + elseif step==AIRBOSS.PatternStep.UPWIND then + + if hornet then + alt=UTILS.FeetToMeters(800) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + end + + elseif step==AIRBOSS.PatternStep.EARLYBREAK then + + if hornet then + alt=UTILS.FeetToMeters(800) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + end + + elseif step==AIRBOSS.PatternStep.LATEBREAK then + + if hornet then + alt=UTILS.FeetToMeters(800) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + end + + elseif step==AIRBOSS.PatternStep.ABEAM then + + if hornet then + alt=UTILS.FeetToMeters(600) + elseif skyhawk then + alt=UTILS.FeetToMeters(500) + end + + aoa=8.1 + + dist=UTILS.NMToMeters(1.2) + + elseif step==AIRBOSS.PatternStep.NINETY then + + if hornet then + alt=UTILS.FeetToMeters(500) + elseif skyhawk then + alt=UTILS.FeetToMeters(500) + end + + aoa=8.1 + + elseif step==AIRBOSS.PatternStep.WAKE then + + if hornet then + alt=UTILS.FeetToMeters(370) + elseif skyhawk then + alt=UTILS.FeetToMeters(370) --? + end + + aoa=8.1 + + elseif step==AIRBOSS.PatternStep.FINAL then + + if hornet then + alt=UTILS.FeetToMeters(300) + elseif skyhawk then + alt=UTILS.FeetToMeters(300) --? + end + + aoa=8.1 + + end end @@ -1162,20 +1260,18 @@ end --- Check marshal and pattern queues. -- @param #AIRBOSS self function AIRBOSS:_CheckQueue() - - local npattern=0 - local nmarshal=#self.Qmarshal - - for _,_flight in pairs(self.Qpattern) do - local flight=_flight --#AIRBOSS.Flightitem - npattern=npattern+flight.nunits - end -- Print queues. self:_PrintQueue(self.flights, "All Flights") self:_PrintQueue(self.Qmarshal, "Marshal") self:_PrintQueue(self.Qpattern, "Pattern") + -- Get number of aircraft units(!) currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + -- Get number of flight groups(!) in marshal pattern. + local nmarshal=#self.Qmarshal + -- Check if there are flights in marshal strack and if the pattern is free. if nmarshal>0 and npattern<1 then @@ -1227,10 +1323,8 @@ end -- @param #string name Queue name. function AIRBOSS:_PrintQueue(queue, name) - local nqueue=#queue - local text=string.format("%s Queue:", name) - if nqueue==0 then + if #queue==0 then text=text.." empty." else for i,_flight in pairs(queue) do @@ -1239,8 +1333,10 @@ function AIRBOSS:_PrintQueue(queue, name) local stack=flight.flag:Get() local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) local fuel=flight.group:GetFuelMin()*100 + local case=flight.case local ai=tostring(flight.ai) - text=text..string.format("\n[%d] %s*%d: alt=%d ft, stack(flag)=%d, time=%s, fuel=%d, ai=%s", i, flight.groupname, flight.nunits, alt, stack, clock, fuel, ai) + text=text..string.format("\n[%d] %s*%d: stackalt=%d ft, flag=%d, case=%d, time=%s, fuel=%d, ai=%s", + i, flight.groupname, flight.nunits, alt, stack, case, clock, fuel, ai) end end self:I(self.lid..text) @@ -1407,6 +1503,7 @@ function AIRBOSS:_CreateFlightGroup(group) flight.actype=group:GetTypeName() flight.onboardnumbers=self:_GetOnboardNumbers(group) flight.section={} + flight.case=self.case if human then @@ -1443,29 +1540,34 @@ function AIRBOSS:_RemoveFlightGroup(group) end end + + --- Orbit at a specified position at a specified alititude with a specified speed. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -- @param #AIRBOSS.Flightitem flight Flight group. function AIRBOSS:_MarshalPlayer(playerData, flight) - - -- Number of full marshal stacks. - local nstacks=#self.Qmarshal -- Check if flight is known to the airboss already. if playerData and flight then + -- Number of flight groups in stack. + local ngroups,nunits=self:_GetQueueInfo(self.Qmarshal, self.case) + + -- Assign next free stack to this flight. + local mystack=ngroups+1 + -- Add group to marshal stack. - self:_AddMarshallGroup(flight, nstacks+1) + self:_AddMarshallGroup(flight, mystack) -- Set step to holding. playerData.step=AIRBOSS.PatternStep.HOLDING - -- Set values for all flights in section. + -- Set same stack for all flights in section. for _,_flight in pairs(flight.section) do local flight=_flight --#AIRBOSS.Flightitem flight.player.step=AIRBOSS.PatternStep.HOLDING - flight.flag:Set(nstacks+1) + flight.flag:Set(mystack) end else @@ -1553,15 +1655,19 @@ end --- Get marshal altitude and position. -- @param #AIRBOSS self -- @param #number stack Assigned stack number. Counting starts at one, i.e. stack=1 is the first stack. +-- @param #number case Recovery case. Default is self.case. -- @return #number Holding altitude in meters. -- @return Core.Point#COORDINATE Holding position coordinate. -- @return Core.Point#COORDINATE Second holding position coordinate of racetrack pattern for CASE II/III recoveries. -function AIRBOSS:_GetMarshalAltitude(stack) +function AIRBOSS:_GetMarshalAltitude(stack, case) -- Stack <= 0. if stack<=0 then return 0,nil,nil end + + -- Recovery case. + case=case or self.case -- Carrier position. local Carrier=self:GetCoordinate() @@ -1573,7 +1679,7 @@ function AIRBOSS:_GetMarshalAltitude(stack) local p1=nil --Core.Point#COORDINATE local p2=nil --Core.Point#COORDINATE - if self.case==1 then + if case==1 then -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next stack. angels0=2 Dist=UTILS.NMToMeters(2.5) @@ -1592,12 +1698,32 @@ function AIRBOSS:_GetMarshalAltitude(stack) return altitude, p1, p2 end +--- Get next free stack. +-- @param #AIRBOSS self +-- @param #number case Recovery case +-- @return #number Smalest (lowest) free stack. +function AIRBOSS:_GetFreeStack(case) + + case=case or self.case + + local stack + if case==1 then + stack=self:_GetQueueInfo(self.Qmarshal, 1) + else + stack=self:_GetQueueInfo(self.Qmarshal, 23) + end + + return stack+1 +end + + --- Get number of groups and units in queue. -- @param #AIRBOSS self -- @param #table queue The queue. Can me all, marshal or pattern. --- @return #number Number of aircraft. --- @return #number Number of flight groups. -function AIRBOSS:_GetQueueInfo(queue) +-- @param #number case (Optional) Count only flights which are in a specific recovery case. +-- @return #number Total Number of flight groups in queue. +-- @return #number Total number of aircraft in queue. +function AIRBOSS:_GetQueueInfo(queue, case) local ngroup=0 local nunits=0 @@ -1605,8 +1731,17 @@ function AIRBOSS:_GetQueueInfo(queue) for _,_flight in pairs(queue) do local flight=_flight --#AIRBOSS.Flightitem - ngroup=ngroup+1 - nunits=nunits+flight.nunits + if case then + + if flight.case==case or (case==23 and (flight.case==2 or flight.case==3)) then + ngroup=ngroup+1 + nunits=nunits+flight.nunits + end + + else + ngroup=ngroup+1 + nunits=nunits+flight.nunits + end end @@ -1622,6 +1757,9 @@ function AIRBOSS:_AddMarshallGroup(flight, flagvalue) -- Set flag value. This corresponds to the stack number which starts at 1. flight.flag:Set(flagvalue) + -- Set recovery case. + flight.case=self.case + -- Pressure. local P=UTILS.hPa2inHg(self:GetCoordinate():GetPressure()) @@ -1629,12 +1767,12 @@ function AIRBOSS:_AddMarshallGroup(flight, flagvalue) -- TODO: Get correct board number if possible? local boardnumber=tostring(flight.onboardnumbers[unitname]) - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(flagvalue)) + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(flagvalue, flight.case)) local brc=self:_BaseRecoveryCourse() -- Marshal message. -- TODO: Get charlie time estimate. - local text=string.format("%s, Case %d, BRC is %03d, hold at %d. Expected Charlie Time XX.\n", boardnumber, self.case, brc, alt) + local text=string.format("%s, Case %d, BRC is %03d, hold at %d. Expected Charlie Time XX.\n", boardnumber, flight.case, brc, alt) text=text..string.format("Altimeter %.2f. Report see me.", P) MESSAGE:New(text, 30):ToAll() @@ -1647,16 +1785,16 @@ end -- @param #AIRBOSS self function AIRBOSS:_CheckCollapseMarshalStack() - -- First flight to enter the landing pattern. + -- Next flight in line. local flight=self.Qmarshal[1] --#AIRBOSS.Flightitem -- TODO: better message. - local text=string.format("%s, you are cleared for Case %d recovery pattern!", flight.groupname, self.case) + local text=string.format("%s, you are cleared for Case %d recovery pattern!", flight.groupname, flight.case) -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence. if flight.ai then -- Collapse stack and send AI to pattern. - self:_CollapseMarshalStack() + self:_CollapseMarshalStack(flight.case) else -- TODO only if skil is not TOPGUN text=text..string.format("\nUse F10 radio menu \"Commence!\" command when you are ready!") @@ -1668,34 +1806,49 @@ end --- Collapse marshal stack. -- @param #AIRBOSS self -function AIRBOSS:_CollapseMarshalStack() +-- @param #number case Recovery case. +function AIRBOSS:_CollapseMarshalStack(case) -- Decrease flag values of all flight groups in marshal stack. for _,_flight in pairs(self.Qmarshal) do local flight=_flight --#AIRBOSS.Flightitem - -- Get current flag/stack value. - local flagvalue=flight.flag:Get() - - -- Decrease by one. - flight.flag:Set(flagvalue-1) - - -- Also decrease flag for section members of flight. - for _,_sec in pairs(flight.section) do - local sec=_sec --#AIRBOSS.Flightitem - sec.flag:Set(flagvalue-1) - end + -- Only collaps stack of which the flight left. CASE II/III stack is the same. + if (case==1 and flight.case==1) or (case>1 and flight.case>1) then + -- Get current flag/stack value. + local stack=flight.flag:Get() + + -- Decrease by one. + flight.flag:Set(stack-1) + + -- Inform players. + if flight.player then + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack-1,case)) + local text=string.format("descent to next lower stack at %d ft", alt) + self:MessageToPlayer(flight.player, text, "MARSHAL", nil, 10) + end + + -- Also decrease flag for section members of flight. + for _,_sec in pairs(flight.section) do + local sec=_sec --#AIRBOSS.Flightitem + sec.flag:Set(stack-1) + end + + end end + --[[ -- Number of marshal flight groups. local nmarshal=#self.Qmarshal -- TODO: collapse marshal stack only from N to N-x. For example, when a group in the stack leaves (e.g. for refueling). for i=nmarshal,1,-1 do local flight=self.Qmarshal[i] --#AIRBOSS.Flightitem + --flight. end + ]] -- First flight to enter the landing pattern. local flight=self.Qmarshal[1] --#AIRBOSS.Flightitem @@ -2494,7 +2647,7 @@ function AIRBOSS:_BullsEye(playerData) return end - -- Check if we are in front of the boat (diffX > 0). + -- Check that we reached the position. if self:_CheckLimits(X, Z, self.C3BullsEye) then -- Get altitiude. @@ -2532,8 +2685,7 @@ function AIRBOSS:_Upwind(playerData) -- Check if we are in front of the boat (diffX > 0). if self:_CheckLimits(X, Z, self.Upwind) then - -- Get altitiude. - local altitude=playerData.unit:GetAltitude() + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) -- Get altitude. local hint, debrief=self:_AltitudeCheck(playerData, self.Upwind, altitude) @@ -2653,18 +2805,16 @@ function AIRBOSS:_Abeam(playerData) -- Check nest step threshold. if self:_CheckLimits(X, Z, self.Abeam) then - -- Get AoA and altitude. - local aoa = playerData.unit:GetAoA() - local alt = playerData.unit:GetAltitude() + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) -- Grade Altitude. - local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, self.Abeam, alt) + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) -- Grade AoA. - local hintAoA, debriefAoA=self:_AoACheck(playerData, self.Abeam, aoa) + local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) -- Grade distance to carrier. - local hintDist, debriefDist=self:_DistanceCheck(playerData, self.Abeam, math.abs(Z)) + local hintDist, debriefDist=self:_DistanceCheck(playerData, dist) --math.abs(Z) -- Compile full hint. local hint=string.format("%s\n%s\n%s", hintAlt, hintAoA, hintDist) @@ -2701,15 +2851,13 @@ function AIRBOSS:_Ninety(playerData) -- At the 90, i.e. 90 degrees between player heading and BRC of carrier. if relheading<=90 then - -- Get altitude and aoa. - local alt=playerData.unit:GetAltitude() - local aoa=playerData.unit:GetAoA() + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) -- Grade altitude. - local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, self.Ninety, alt) + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) -- Grade AoA. - local hintAoA, debriefAoA=self:_AoACheck(playerData, self.Ninety, aoa) + local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) -- Compile full hint. local hint=string.format("%s\n%s", hintAlt, hintAoA) @@ -2747,16 +2895,14 @@ function AIRBOSS:_Wake(playerData) -- Right behind the wake of the carrier dZ>0. if self:_CheckLimits(X, Z, self.Wake) then - - -- Get player altitude and AoA. - local alt=playerData.unit:GetAltitude() - local aoa=playerData.unit:GetAoA() + + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) -- Grade altitude. - local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, self.Wake, alt) + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) -- Grade AoA. - local hintAoA, debriefAoA=self:_AoACheck(playerData, self.Wake, aoa) + local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) -- Compile full hint. local hint=string.format("%s\n%s", hintAlt, hintAoA) @@ -2793,15 +2939,13 @@ function AIRBOSS:_Final(playerData) if math.abs(lineup)<5 and math.abs(relhead)<10 then - -- Get player altitude and AoA. - local alt = playerData.unit:GetAltitude() - local aoa = playerData.unit:GetAoA() + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) -- Grade altitude. - local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, self.Groove, alt) + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) -- AoA feed back - local hintAoA, debriefAoA=self:_AoACheck(playerData, self.Groove, aoa) + local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) -- Compile full hint. local hint=string.format("%s\n%s", hintAlt, hintAoA) @@ -3765,16 +3909,17 @@ function AIRBOSS:_GetGoodBadScore(playerData) return lowscore, badscore end + + --- Evaluate player's altitude at checkpoint. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. --- @param #AIRBOSS.Checkpoint checkpoint Checkpoint. --- @param #number altitude Player's current altitude in meters. +-- @param #number altopt Optimal alitude in meters. -- @return #string Feedback text. -- @return #string Debriefing text. -function AIRBOSS:_AltitudeCheck(playerData, checkpoint, altitude) +function AIRBOSS:_AltitudeCheck(playerData, altopt) - if checkpoint.Altitude==nil then + if altopt==nil then return nil, nil end @@ -3785,7 +3930,7 @@ function AIRBOSS:_AltitudeCheck(playerData, checkpoint, altitude) local lowscore, badscore=self:_GetGoodBadScore(playerData) -- Altitude error +-X% - local _error=(altitude-checkpoint.Altitude)/checkpoint.Altitude*100 + local _error=(altitude-altopt)/altopt*100 local hint if _error>badscore then @@ -3818,21 +3963,23 @@ end --- Evaluate player's altitude at checkpoint. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. --- @param #AIRBOSS.Checkpoint checkpoint Checkpoint. --- @param #number distance Player's current distance to the boat in meters. +-- @param #number optdist Optimal distance in meters. -- @return #string Feedback message text. -- @return #string Debriefing text. -function AIRBOSS:_DistanceCheck(playerData, checkpoint, distance) +function AIRBOSS:_DistanceCheck(playerData, optdist) - if checkpoint.Distance==nil then + if optdist==nil then return nil, nil end + + -- Distance to carrier. + local distance=playerData.unit:GetCoodinate():Get2DDistance(self:GetCoordinate()) -- Get relative score. local lowscore, badscore = self:_GetGoodBadScore(playerData) -- Altitude error +-X% - local _error=(distance-checkpoint.Distance)/checkpoint.Distance*100 + local _error=(distance-optdist)/optdist*100 local hint if _error>badscore then @@ -3865,21 +4012,23 @@ end --- Score for correct AoA. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. --- @param #AIRBOSS.Checkpoint checkpoint Checkpoint. --- @param #number aoa Player's current Angle of attack. +-- @param #number optaoa Optimal AoA. -- @return #string Feedback message text or easy and normal difficulty level or nil for hard. -- @return #string Debriefing text. -function AIRBOSS:_AoACheck(playerData, checkpoint, aoa) +function AIRBOSS:_AoACheck(playerData, optaoa) - if checkpoint.AoA==nil then + if optaoa==nil then return nil, nil end -- Get relative score. local lowscore, badscore = self:_GetGoodBadScore(playerData) + -- Player AoA + local aoa=playerData.unit:GetAoA() + -- Altitude error +-X% - local _error=(aoa-checkpoint.AoA)/checkpoint.AoA*100 + local _error=(aoa-optaoa)/optaoa*100 local hint if _error>badscore then --Slow @@ -3924,21 +4073,6 @@ end function AIRBOSS:_Debrief(playerData) self:F("Debriefing") - -- Debriefing text. - local text=string.format("Debriefing:\n") - text=text..string.format("================================\n") - for _,_data in pairs(playerData.debrief) do - local step=_data.step - local comment=_data.hint - text=text..string.format("* %s:\n",step) - text=text..string.format("%s\n", comment) - end - - local text="Your detailed debriefing can now be seen in F10 radio menu." - - -- Send debrief message to player - self:_SendMessageToPlayer(text, 30, playerData, true, "Paddles") - -- LSO grade, points, and flight data analyis. local grade, points, analysis=self:_LSOgrade(playerData) @@ -3951,8 +4085,9 @@ function AIRBOSS:_Debrief(playerData) table.insert(playerData.grades, mygrade) -- LSO grade message. - text=string.format("%s %.1f PT - %s", grade, points, analysis) - self:_SendMessageToPlayer(text, 10, playerData, true, "Paddles", 30) + local text=string.format("%s %.1f PT - %s", grade, points, analysis) + text=text..string.format("Your detailed debriefing can now be seen in F10 radio menu.") + self:MessageToPlayer(playerData,text, "LSO","" , 30, true) -- New approach. if playerData.boltered or playerData.waveoff or playerData.patternwo then @@ -4068,11 +4203,11 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -- @param #string message The message to send. --- @param #string sender The person who sends the message. --- @param #string receiver The person who receives the message. --- @param #number duration Display message duration. Default 5 sec. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to \"\" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. -- @param #boolean clear If true, clear screen from previous messages. --- @param #number delay Delay in seconds, before the message is send. +-- @param #number delay Delay in seconds, before the message is displayed. function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay) if playerData and message and message~="" then @@ -4472,7 +4607,7 @@ function AIRBOSS:_RequestStraightIn(_unitName) -- Get flight group. local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) - local text + local text if flight then -- Get stack value. @@ -4484,7 +4619,7 @@ function AIRBOSS:_RequestStraightIn(_unitName) else -- Number of aircraft currently in pattern. - local npattern=self:_GetQueueInfo(self.Qpattern) + local _,npattern=self:_GetQueueInfo(self.Qpattern) -- TODO: set nmax for pattern. Should be ~6 but let's make this 4. if npattern>0 then @@ -4498,7 +4633,7 @@ function AIRBOSS:_RequestStraightIn(_unitName) playerData.step=AIRBOSS.PatternStep.COMMENCING -- Collaps marshal stack. - self:_CollapseMarshalStack() + self:_CollapseMarshalStack(flight.case) end end From 39ffc28cb415f7e81a86d769b0c8c2cb6a8f7b55 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Nov 2018 23:45:24 +0100 Subject: [PATCH 052/147] AIRBOSS v0.3.2 --- Moose Development/Moose/Ops/Airboss.lua | 1176 +++++++++++++---------- 1 file changed, 647 insertions(+), 529 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index c1642a214..8e0c11213 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -17,8 +17,9 @@ -- * Multiple carriers supported (due to object oriented approach). -- -- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much work in progress. +-- -- At the moment training parameters are optimized for F/A-18C Hornet as aircraft and USS John C. Stennis as carrier. --- Other aircraft and carriers **might** be possible in future but would need a different set of parameters. +-- Other aircraft and carriers **might** be possible in future but would need a different set of optimized parameters. -- -- === -- @@ -58,10 +59,10 @@ -- @field #AIRBOSS.Checkpoint Wake Right behind the carrier. -- @field #AIRBOSS.Checkpoint Groove In the groove checkpoint. -- @field #AIRBOSS.Checkpoint Trap Landing checkpoint. --- @field #AIRBOSS.Checkpoint C3Descent4k Case II/III descent at 4000 ft/min right after leaving holding pattern. --- @field #AIRBOSS.Checkpoint C3Descent2k Case II/III descent at 2000 ft/min at 5000 ft plattform. --- @field #AIRBOSS.Checkpoint C3DirtyUp Case II/III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. --- @field #AIRBOSS.Checkpoint C3BullsEye Case III intercept glideslope and follow ICLS "bullseye". +-- @field #AIRBOSS.Checkpoint Descent4k Case II/III descent at 4000 ft/min right after leaving holding pattern. +-- @field #AIRBOSS.Checkpoint Platform Case II/III descent at 2000 ft/min at 5000 ft platform. +-- @field #AIRBOSS.Checkpoint DirtyUp Case II/III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. +-- @field #AIRBOSS.Checkpoint Bullseye Case III intercept glideslope and follow ICLS aka "bullseye". -- @field #number case Recovery case I, II or III in progress. -- @field #table flights List of all flights in the CCA. -- @field #table Qmarshal Queue of marshalling aircraft groups. @@ -114,10 +115,10 @@ AIRBOSS = { Wake = {}, Groove = {}, Trap = {}, - C3Descent4k = {}, - C3Descent2k = {}, - C3DirtyUp = {}, - C3BullsEye = {}, + Descent4k = {}, + Platform = {}, + DirtyUp = {}, + Bullseye = {}, case = 1, flights = {}, Qpattern = {}, @@ -137,7 +138,7 @@ AIRBOSS = { AIRBOSS.AircraftPlayer={ AV8B="AV8BNA", HORNET="FA-18C_hornet", - A4EC="A-4E-C", --TODO: Correct string? + A4EC="A-4E-C", } --- Aircraft types capable of landing on carrier (human+AI). @@ -153,7 +154,7 @@ AIRBOSS.AircraftPlayer={ AIRBOSS.AircraftCarrier={ AV8B="AV8BNA", HORNET="FA-18C_hornet", - A4EC="A-4E-C", --TODO: Correct string? + A4EC="A-4E-C", S3B="S-3B", S3BTANKER="S-3B Tanker", E2D="E-2C", @@ -198,7 +199,7 @@ AIRBOSS.PatternStep={ COMMENCING="Commencing", HOLDING="Holding", DESCENT4K="Descent 4000 ft/min", - DESCENT2K="Platform", + PLATFORM="Platform", DIRTYUP="Level out and Dirty Up", BULLSEYE="Follow Bullseye", INITIAL="Initial", @@ -435,7 +436,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.1" +AIRBOSS.version="0.3.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -995,134 +996,138 @@ function AIRBOSS:_InitStennis() q2:BigSmokeSmall(0.1)--:SmokeBlue() ]] - -- 4k descent from holding pattern to 5k platform - self.C3Descent4k.name="4k Descent" - self.C3Descent4k.Xmin=-UTILS.NMToMeters(50) - self.C3Descent4k.Xmax=-UTILS.NMToMeters(20) - self.C3Descent4k.Zmin=-UTILS.NMToMeters(10) - self.C3Descent4k.Zmax= UTILS.NMToMeters(3) - self.C3Descent4k.LimitXmin=nil - self.C3Descent4k.LimitXmax=-UTILS.NMToMeters(20) --TODO: better rho dist. decrease descent 20 2000 ft/min at 5000 ft alt and user rad alt. - self.C3Descent4k.LimitZmin=nil - self.C3Descent4k.LimitZmax=nil + -- 4k descent from holding pattern to 5k platform. + self.Descent4k.name="Descent 4k" + self.Descent4k.Xmin=-UTILS.NMToMeters(50) -- Not more than 50 NM behind the boat. + self.Descent4k.Xmax=-UTILS.NMToMeters(20) -- Not more than 20 NM closer to the boat from behind. + self.Descent4k.Zmin=-UTILS.NMToMeters(15) -- Not more than 15 NM port/left of boat. + self.Descent4k.Zmax= UTILS.NMToMeters(5) -- Not more than 5 NM starboard/right of boat. + self.Descent4k.LimitXmin=nil + --TODO: better rho dist. decrease descent 20 2000 ft/min at 5000 ft alt and user rad alt. + self.Descent4k.LimitXmax=-UTILS.NMToMeters(21) -- Check and next step when 21 NM behind the boat. + self.Descent4k.LimitZmin=nil + self.Descent4k.LimitZmax=nil - -- 2k descent from 5k platform to 1200 dirty up level flight. - self.C3Descent2k.name="2k Descent" - self.C3Descent2k.Xmin=-UTILS.NMToMeters(21) - self.C3Descent2k.Xmax=nil - self.C3Descent2k.Zmin=-UTILS.NMToMeters(30) - self.C3Descent2k.Zmax= UTILS.NMToMeters(30) - self.C3Descent2k.LimitXmin=nil - self.C3Descent2k.LimitXmax=-UTILS.NMToMeters(12) --TODO: better rho dist! now switch to dirty up level flight 12 NM. - self.C3Descent2k.LimitZmin=nil - self.C3Descent2k.LimitZmax=nil + -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight. + self.Platform.name="Platform 5k" + self.Platform.Xmin=-UTILS.NMToMeters(22) -- Not more than 22 NM behind the boat. Last check was at 21 NM. + self.Platform.Xmax =nil + self.Platform.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. + self.Platform.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. + self.Platform.LimitXmin=nil + --TODO: better rho dist! now switch to dirty up level flight 12 NM. + self.Platform.LimitXmax=-UTILS.NMToMeters(20) -- Check and next step when 20 NM behind the boat. + self.Platform.LimitZmin=nil + self.Platform.LimitZmax=nil -- Level out at 1200 ft and dirty up. - self.C3DirtyUp.name="Dirty Up" - self.C3DirtyUp.Xmin=-UTILS.NMToMeters(13) - self.C3DirtyUp.Xmax=nil - self.C3DirtyUp.Zmin=-UTILS.NMToMeters(30) - self.C3DirtyUp.Zmax= UTILS.NMToMeters(30) - self.C3DirtyUp.LimitXmin=nil - self.C3DirtyUp.LimitXmax=-UTILS.NMToMeters(3) --TODO: better rho dist! Intercept glideslope and follow bullseye. - self.C3DirtyUp.LimitZmin=nil - self.C3DirtyUp.LimitZmax=nil + self.DirtyUp.name="Dirty Up" + self.DirtyUp.Xmin=-UTILS.NMToMeters(21) -- Not more than 21 NM behind the boat. + self.DirtyUp.Xmax= nil + self.DirtyUp.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. + self.DirtyUp.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. + self.DirtyUp.LimitXmin=nil + --TODO: better rho dist! Intercept glideslope and follow bullseye. + self.DirtyUp.LimitXmax=-UTILS.NMToMeters(10) -- Check and next step at 10 NM behind the boat. + self.DirtyUp.LimitZmin=nil + self.DirtyUp.LimitZmax=nil -- Intercept glide slope and follow bullseye. self.Bullseye.name="Bullseye" - self.Bullseye.Xmin=-UTILS.NMToMeters(4) - self.Bullseye.Xmax=nil - self.Bullseye.Zmin=-UTILS.NMToMeters(30) - self.Bullseye.Zmax= UTILS.NMToMeters(30) + self.Bullseye.Xmin=-UTILS.NMToMeters(11) -- Not more than 11 NM behind the boat. Last check was at 10 NM. + self.Bullseye.Xmax= nil + self.Bullseye.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port. + self.Bullseye.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard. self.Bullseye.LimitXmin=nil - self.Bullseye.LimitXmax=-UTILS.NMToMeters(1) --TODO: better rho dist! Call the ball. + --TODO: better rho dist! Call the ball. + self.Bullseye.LimitXmax=-UTILS.NMToMeters(3) -- Check and next step 3 NM behind the boat. self.Bullseye.LimitZmin=nil - self.C3DirtyUp.LimitZmax=nil + self.Bullseye.LimitZmax=nil - -- Upwind leg + -- Upwind leg or break entry. self.Upwind.name="Upwind" - self.Upwind.Xmin=-UTILS.NMToMeters(4) - self.Upwind.Xmax=nil - self.Upwind.Zmin=-100 - self.Upwind.Zmax=1000 - self.Upwind.LimitXmin=0 + self.Upwind.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of ?. + self.Upwind.Xmax= nil + self.Upwind.Zmin=-100 -- Not more than 100 meters port of boat. + self.Upwind.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard of boat. + self.Upwind.LimitXmin=0 -- Check and next step when at carrier and starboard of carrier. self.Upwind.LimitXmax=nil self.Upwind.LimitZmin=0 self.Upwind.LimitZmax=nil - -- Early break + -- Early break. self.BreakEarly.name="Early Break" - self.BreakEarly.Xmin=-500 - self.BreakEarly.Xmax=UTILS.NMToMeters(5) - self.BreakEarly.Zmin=-UTILS.NMToMeters(2) - self.BreakEarly.Zmax=UTILS.NMToMeters(1) - self.BreakEarly.LimitXmin=0 - self.BreakEarly.LimitXmax=nil - self.BreakEarly.LimitZmin=-370 -- 0.2 NM port of carrier - self.BreakEarly.LimitZmax=nil + self.BreakEarly.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakEarly.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakEarly.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.BreakEarly.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakEarly.LimitXmin= 0 -- Check and next step 0.2 NM port and in front of boat. + self.BreakEarly.LimitXmax= nil + self.BreakEarly.LimitZmin=-UTILS.NMToMeters(0.2) -- -370 m port + self.BreakEarly.LimitZmax= nil -- Late break self.BreakLate.name="Late Break" - self.BreakLate.Xmin=-500 - self.BreakLate.Xmax=UTILS.NMToMeters(5) - self.BreakLate.Zmin=-UTILS.NMToMeters(2) - self.BreakLate.Zmax=UTILS.NMToMeters(1) - self.BreakLate.LimitXmin=0 - self.BreakLate.LimitXmax=nil - self.BreakLate.LimitZmin=-1470 --0.8 NM - self.BreakLate.LimitZmax=nil + self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax= nil + self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.8) -- -1470 m port + self.BreakLate.LimitZmax= 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.Xmin= nil + self.Abeam.Xmax= nil + self.Abeam.Zmin=-UTILS.NMToMeters(3) -- Not more than 3 NM port. + self.Abeam.Zmax= 0 -- Must be port! + self.Abeam.LimitXmin=-200 -- Check and next step 200 meters behind the ship. + self.Abeam.LimitXmax= nil + self.Abeam.LimitZmin= nil + self.Abeam.LimitZmax= nil -- 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.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. LIG check anyway. + self.Ninety.Xmax= 0 -- Must be behind the boat. + self.Ninety.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port of boat. + self.Ninety.Zmax= nil self.Ninety.LimitXmin=nil self.Ninety.LimitXmax=nil self.Ninety.LimitZmin=nil - self.Ninety.LimitZmax=-1111 + self.Ninety.LimitZmax=-UTILS.NMToMeters(0.6) -- Check and next step when 0.6 NM port. -- Wake position self.Wake.name="Wake" - self.Wake.Xmin=-4000 - self.Wake.Xmax=0 - self.Wake.Zmin=-2000 - self.Wake.Zmax=nil + self.Wake.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Wake.Xmax= 0 -- Must be behind the boat. + self.Wake.Zmin=-2000 -- Not more than 2 km port of boat. + self.Wake.Zmax= nil self.Wake.LimitXmin=nil self.Wake.LimitXmax=nil - self.Wake.LimitZmin=0 + self.Wake.LimitZmin=0 -- Check and next step when directly behind the boat. self.Wake.LimitZmax=nil -- In the groove self.Groove.name="Groove" - self.Groove.Xmin=-4000 - self.Groove.Xmax= 100 - self.Groove.Zmin=-1000 - self.Groove.Zmax=nil - self.Groove.LimitXmin=nil + self.Groove.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Groove.Xmax= 0 -- Must be behind the boat. + self.Groove.Zmin=-1000 -- Not more than 1 km port. + self.Groove.Zmax= nil + self.Groove.LimitXmin=nil -- No limits. Check is carried out differently. self.Groove.LimitXmax=nil self.Groove.LimitZmin=nil self.Groove.LimitZmax=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.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Trap.Xmax= nil + self.Trap.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port + self.Trap.Zmax= UTILS.NMToMeters(2) -- Not more than 2 NM starboard. + self.Trap.LimitXmin=nil -- No limits. Check is carried out differently. self.Trap.LimitXmax=nil self.Trap.LimitZmin=nil self.Trap.LimitZmax=nil @@ -1130,6 +1135,7 @@ function AIRBOSS:_InitStennis() end --- Get optimal aircraft flight parameters at checkpoint. +-- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -- @param #string step Pattern step. -- @return #number Altitude in meters or nil. @@ -1151,7 +1157,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) speed=UTILS.KnotsToMps(250) - elseif step==AIRBOSS.PatternStep.DESCENT2K then + elseif step==AIRBOSS.PatternStep.PLATFORM then alt=UTILS.FeetToMeters(5000) @@ -1311,7 +1317,7 @@ function AIRBOSS:_CheckQueue() -- Two minutes in pattern at least and >45 sec interval between pattern flights. if self:IsRecovering() and Tmarshal>TmarshalMin and Tpattern>TpatternMin then - self:_CheckCollapseMarshalStack() + self:_CheckCollapseMarshalStack(marshalflight) end end @@ -1783,10 +1789,8 @@ end --- Check if marshal stack can be collapsed. -- If next in line is an AI flight, this is done. If human player is next, we wait for "Commence" via F10 radio menu command. -- @param #AIRBOSS self -function AIRBOSS:_CheckCollapseMarshalStack() - - -- Next flight in line. - local flight=self.Qmarshal[1] --#AIRBOSS.Flightitem +-- @param #AIRBOSS.Flightitem flight Flight to go to pattern. +function AIRBOSS:_CheckCollapseMarshalStack(flight) -- TODO: better message. local text=string.format("%s, you are cleared for Case %d recovery pattern!", flight.groupname, flight.case) @@ -1794,7 +1798,7 @@ function AIRBOSS:_CheckCollapseMarshalStack() -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence. if flight.ai then -- Collapse stack and send AI to pattern. - self:_CollapseMarshalStack(flight.case) + self:_CollapseMarshalStack(flight) else -- TODO only if skil is not TOPGUN text=text..string.format("\nUse F10 radio menu \"Commence!\" command when you are ready!") @@ -1806,8 +1810,14 @@ end --- Collapse marshal stack. -- @param #AIRBOSS self --- @param #number case Recovery case. -function AIRBOSS:_CollapseMarshalStack(case) +-- @param #AIRBOSS.Flightitem patternflight Flight to go to pattern. +-- @param #boolean refuel If true, patternflight wants to refuel and not go into pattern. +function AIRBOSS:_CollapseMarshalStack(patternflight, refuel) + self:I({flight=patternflight, refuel=refuel}) + + -- Recovery case of flight. + local case=patternflight.case + local pstack=patternflight.flag:Get() -- Decrease flag values of all flight groups in marshal stack. for _,_flight in pairs(self.Qmarshal) do @@ -1817,52 +1827,66 @@ function AIRBOSS:_CollapseMarshalStack(case) if (case==1 and flight.case==1) or (case>1 and flight.case>1) then -- Get current flag/stack value. - local stack=flight.flag:Get() + local mstack=flight.flag:Get() - -- Decrease by one. - flight.flag:Set(stack-1) + -- Only collapse stacks above the new pattern flight. + -- TODO: this will go wrong, if patternflight is not in marshal stack because it will have value -100 and all mstacks will be larger! + -- Maybe need to set the initial value to 1000? Or check pstack>0? + if pstack>0 and mstack>pstack then - -- Inform players. - if flight.player then - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack-1,case)) - local text=string.format("descent to next lower stack at %d ft", alt) - self:MessageToPlayer(flight.player, text, "MARSHAL", nil, 10) - end - - -- Also decrease flag for section members of flight. - for _,_sec in pairs(flight.section) do - local sec=_sec --#AIRBOSS.Flightitem - sec.flag:Set(stack-1) + -- Decrease stack/flag by one ==> AI will go lower. + flight.flag:Set(mstack-1) + + -- Inform players. + if flight.player then + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(mstack-1,case)) + local text=string.format("descent to next lower stack at %d ft", alt) + self:MessageToPlayer(flight.player, text, "MARSHAL", nil, 10) + end + + -- Also decrease flag for section members of flight. + for _,_sec in pairs(flight.section) do + local sec=_sec --#AIRBOSS.Flightitem + sec.flag:Set(mstack-1) + end + end end end - --[[ - -- Number of marshal flight groups. - local nmarshal=#self.Qmarshal - -- TODO: collapse marshal stack only from N to N-x. For example, when a group in the stack leaves (e.g. for refueling). - for i=nmarshal,1,-1 do - local flight=self.Qmarshal[i] --#AIRBOSS.Flightitem - - --flight. - end - ]] + if refuel then - -- First flight to enter the landing pattern. - local flight=self.Qmarshal[1] --#AIRBOSS.Flightitem + -- Debug + self:I(self.lid..string.format("Flight %s is going for gas.", patternflight.groupname)) - self:I(self.lid..string.format("New pattern flight %s.", flight.groupname)) + -- New time stamp for time in pattern. + patternflight.time=timer.getAbsTime() + + -- Set flag to -1. + patternflight.flag:Set(-1) + + -- TODO: Add to refueling queue. - -- New time stamp for time in pattern. - flight.time=timer.getAbsTime() - - -- Add flight to pattern queue. - table.insert(self.Qpattern, flight) - - -- Remove flight from marshal queue. - table.remove(self.Qmarshal, 1) + else + + -- Debug + self:I(self.lid..string.format("Flight %s is going into pattern.", patternflight.groupname)) + + -- New time stamp for time in pattern. + patternflight.time=timer.getAbsTime() + + -- Decrease flag. + patternflight.flag:Set(pstack-1) + + -- Add flight to pattern queue. + table.insert(self.Qpattern, patternflight) + + -- Remove flight from marshal queue. + self:_RemoveGroupFromQueue(self.Qmarshal, patternflight.group) + + end end --- Remove a group from a queue. @@ -1872,13 +1896,14 @@ end function AIRBOSS:_RemoveGroupFromQueue(queue, group) local name=group:GetName() - + for i,_flight in pairs(queue) do local flight=_flight --#AIRBOSS.Flightitem if flight.groupname==name then self:I(self.lid..string.format("Removing group %s from queue.", name)) table.remove(queue, i) + return end end @@ -1991,10 +2016,10 @@ function AIRBOSS:_CheckPlayerStatus() -- CASE II/III: Initial descent with 4000 ft/min. self:_Descent4k(playerData) - elseif playerData.step==AIRBOSS.PatternStep.DESCENT2K then + elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then -- CASE II/III: Player has reached 5k "Platform". - self:_Descent2k(playerData) + self:_Platform(playerData) elseif playerData.step==AIRBOSS.PatternStep.DIRTYUP then @@ -2004,7 +2029,7 @@ function AIRBOSS:_CheckPlayerStatus() elseif playerData.step==AIRBOSS.PatternStep.BULLSEYE then -- CASE III: Player has intercepted the glide slope and should follow "Bullseye" (ICLS). - self:_BullsEye(playerData) + self:_Bullseye(playerData) elseif playerData.step==AIRBOSS.PatternStep.INITIAL then @@ -2115,7 +2140,9 @@ function AIRBOSS:OnEventBirth(EventData) -- Check if aircraft type the player occupies is carrier capable. local rightaircraft=self:_IsCarrierAircraft(_unit) if rightaircraft==false then - self:E(string.format("Player aircraft type %s not supported by AIRBOSS class.", _unit:GetTypeName())) + local text=string.format("Player aircraft type %s not supported by AIRBOSS class.", _unit:GetTypeName()) + --MESSAGE:New(text, 30, "ERROR", true):ToGroup(_group) + self:E(self.lid..text) return end @@ -2130,26 +2157,11 @@ function AIRBOSS:OnEventBirth(EventData) --self:RadioTransmission(self.LSOradio, self.radiocall.LONGINGROOVE, false, 20) -- Start in the groove for debugging. - self.groovedebug=true + self.groovedebug=false end end ---- Check if aircraft is capable of landing on an aircraft carrier. --- @param #AIRBOSS self --- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.) --- @return #boolean If true, aircraft can land on a carrier. -function AIRBOSS:_IsCarrierAircraft(unit) - local carrieraircraft=false - local aircrafttype=unit:GetTypeName() - for _,actype in pairs(AIRBOSS.AircraftCarrier) do - if actype==aircrafttype then - return true - end - end - return false -end - --- Airboss event handler for event land. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData @@ -2485,7 +2497,13 @@ function AIRBOSS:_Commencing(playerData) -- Initialize player data for new approach. self:_InitPlayer(playerData) - + + -- Commence + local text=string.format("Commencing. Case %d.", self.case) + + -- Message to player. + self:_SendMessageToPlayer(text, 10, playerData) + -- Next step: depends on case recovery. if self.case==1 then -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. @@ -2494,12 +2512,6 @@ function AIRBOSS:_Commencing(playerData) -- CASE III: Player has to start the descent at 4000 ft/min. playerData.step=AIRBOSS.PatternStep.DESCENT4K end - - - local text="Commencing." - - -- Message to player. - self:_SendMessageToPlayer(text, 10, playerData) end --- Start pattern when player enters the initial zone. @@ -2511,7 +2523,7 @@ function AIRBOSS:_Initial(playerData) if playerData.unit:IsInZone(self.zoneInitial) then -- Inform player. - local hint = string.format("Entering the pattern.") + local hint=string.format("Entering the pattern.") if playerData.difficulty==AIRBOSS.Difficulty.EASY then hint=hint.."Aim for 800 feet and 350 kts at the break entry." end @@ -2534,19 +2546,19 @@ function AIRBOSS:_Descent4k(playerData) local X, Z, rho, phi=self:_GetDistances(playerData.unit) -- Abort condition check. - if self:_CheckAbort(X, Z, self.C3Descent4k) then - self:_AbortPattern(playerData, X, Z, self.C3Descent4k) + if self:_CheckAbort(X, Z, self.Descent4k) then + self:_AbortPattern(playerData, X, Z, self.Descent4k) return end -- Check if we are in front of the boat (diffX > 0). - if self:_CheckLimits(X, Z, self.C3Descent4k) then + if self:_CheckLimits(X, Z, self.Descent4k) then - -- Get altitiude. - local altitude=playerData.unit:GetAltitude() + -- Get optimal altitude, distance and speed. + local altitude=self:_GetAircraftParameters(playerData) -- Get altitude. - local hint, debrief=self:_AltitudeCheck(playerData, self.C3Descent4k, altitude) + local hint, debrief=self:_AltitudeCheck(playerData, altitude) -- Message to player self:_SendMessageToPlayer(hint, 10, playerData) @@ -2554,46 +2566,46 @@ function AIRBOSS:_Descent4k(playerData) -- Debrief. self:_AddToSummary(playerData, "Descent 4k", debrief) - -- Next step: Early Break. - playerData.step=AIRBOSS.PatternStep.DESCENT2K + -- Next step: Platform at 5k + playerData.step=AIRBOSS.PatternStep.PLATFORM end end ---- Descent at 2k. +--- Platform at 5k ft. Descent at 2000 ft/min. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Descent2k(playerData) +function AIRBOSS:_Platform(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi=self:_GetDistances(playerData.unit) -- Abort condition check. - if self:_CheckAbort(X, Z, self.C3Descent2k) then - self:_AbortPattern(playerData, X, Z, self.C3Descent2k) + if self:_CheckAbort(X, Z, self.Platform) then + self:_AbortPattern(playerData, X, Z, self.Platform) return end -- Check if we are in front of the boat (diffX > 0). - if self:_CheckLimits(X, Z, self.C3Descent2k) then + if self:_CheckLimits(X, Z, self.Platform) then - -- Get altitiude. - local altitude=playerData.unit:GetAltitude() + -- Get optimal altitiude. + local altitude=self:_GetAircraftParameters(playerData) -- Get altitude. - local hint, debrief=self:_AltitudeCheck(playerData, self.C3Descent2k, altitude) + local hint, debrief=self:_AltitudeCheck(playerData, altitude) -- Message to player self:_SendMessageToPlayer(hint, 10, playerData) -- Debrief. - self:_AddToSummary(playerData, "Descent 2k", debrief) + self:_AddToSummary(playerData, "Platform 5k", debrief) - -- Next step: Early Break. + -- Next step: Dirty up and level out at 1200 ft. playerData.step=AIRBOSS.PatternStep.DIRTYUP end end ---- Dirty up. +--- Dirty up and level out at 1200 ft. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_DirtyUp(playerData) @@ -2602,19 +2614,19 @@ function AIRBOSS:_DirtyUp(playerData) local X, Z, rho, phi=self:_GetDistances(playerData.unit) -- Abort condition check. - if self:_CheckAbort(X, Z, self.C3DirtyUp) then - self:_AbortPattern(playerData, X, Z, self.C3DirtyUp) + if self:_CheckAbort(X, Z, self.DirtyUp) then + self:_AbortPattern(playerData, X, Z, self.DirtyUp) return end -- Check if we are in front of the boat (diffX > 0). - if self:_CheckLimits(X, Z, self.C3DirtyUp) then + if self:_CheckLimits(X, Z, self.DirtyUp) then - -- Get altitiude. - local altitude=playerData.unit:GetAltitude() + -- Get optimal altitiude. + local altitude=self:_GetAircraftParameters(playerData) -- Get altitude. - local hint, debrief=self:_AltitudeCheck(playerData, self.C3DirtyUp, altitude) + local hint, debrief=self:_AltitudeCheck(playerData, altitude) -- Message to player self:_SendMessageToPlayer(hint, 10, playerData) @@ -2633,28 +2645,28 @@ function AIRBOSS:_DirtyUp(playerData) end end ---- Bulls eye. +--- Intercept glide slop and follow ICLS, aka Bullseye. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_BullsEye(playerData) +function AIRBOSS:_Bullseye(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi=self:_GetDistances(playerData.unit) -- Abort condition check. - if self:_CheckAbort(X, Z, self.C3DirtyUp) then - self:_AbortPattern(playerData, X, Z, self.C3BullsEye) + if self:_CheckAbort(X, Z, self.Bullseye) then + self:_AbortPattern(playerData, X, Z, self.Bullseye) return end -- Check that we reached the position. - if self:_CheckLimits(X, Z, self.C3BullsEye) then + if self:_CheckLimits(X, Z, self.Bullseye) then - -- Get altitiude. - local altitude=playerData.unit:GetAltitude() + -- Get optimal altitiude. + local altitude=self:_GetAircraftParameters(playerData) -- Get altitude. - local hint, debrief=self:_AltitudeCheck(playerData, self.C3BullsEye, altitude) + local hint, debrief=self:_AltitudeCheck(playerData, altitude) -- Message to player self:_SendMessageToPlayer(hint, 10, playerData) @@ -2685,10 +2697,11 @@ function AIRBOSS:_Upwind(playerData) -- Check if we are in front of the boat (diffX > 0). if self:_CheckLimits(X, Z, self.Upwind) then + -- Get optimal altitude, distance and speed. local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) -- Get altitude. - local hint, debrief=self:_AltitudeCheck(playerData, self.Upwind, altitude) + local hint, debrief=self:_AltitudeCheck(playerData, alt) -- Message to player self:_SendMessageToPlayer(hint, 10, playerData) @@ -2726,11 +2739,11 @@ function AIRBOSS:_Break(playerData, part) -- Check limits. if self:_CheckLimits(X, Z, breakpoint) then - -- Get current altitude. - local altitude=playerData.unit:GetAltitude() + -- Get optimal altitude, distance and speed. + local altitude=self:_GetAircraftParameters(playerData) -- Grade altitude. - local hint, debrief=self:_AltitudeCheck(playerData, breakpoint, altitude) + local hint, debrief=self:_AltitudeCheck(playerData, altitude) -- Send message to player. self:_SendMessageToPlayer(hint, 10, playerData) @@ -2805,6 +2818,7 @@ function AIRBOSS:_Abeam(playerData) -- Check nest step threshold. if self:_CheckLimits(X, Z, self.Abeam) then + -- Get optimal altitude, distance and speed. local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) -- Grade Altitude. @@ -2851,6 +2865,7 @@ function AIRBOSS:_Ninety(playerData) -- At the 90, i.e. 90 degrees between player heading and BRC of carrier. if relheading<=90 then + -- Get optimal altitude, distance and speed. local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) -- Grade altitude. @@ -2896,6 +2911,7 @@ function AIRBOSS:_Wake(playerData) -- Right behind the wake of the carrier dZ>0. if self:_CheckLimits(X, Z, self.Wake) then + -- Get optimal altitude, distance and speed. local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) -- Grade altitude. @@ -2939,6 +2955,7 @@ function AIRBOSS:_Final(playerData) if math.abs(lineup)<5 and math.abs(relhead)<10 then + -- Get optimal altitude, distance and speed. local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) -- Grade altitude. @@ -3808,12 +3825,16 @@ function AIRBOSS:_CheckAbort(X, Z, pos) local abort=false if pos.Xmin and Xpos.Xmax then + self:E(string.format("Xmax: X=%d > %d=Xmax", X, pos.Xmax)) abort=true elseif pos.Zmin and Zpos.Zmax then + self:E(string.format("Zmax: Z=%d > %d=Zmax", Z, pos.Zmax)) abort=true end @@ -3838,9 +3859,9 @@ function AIRBOSS:_TooFarOutText(X, Z, posData) local ztext=nil if posData.Zmin and ZposData.Zmax then - ztext="starboard (right)" + ztext="starboard (right) of" end if xtext and ztext then @@ -3851,7 +3872,7 @@ function AIRBOSS:_TooFarOutText(X, Z, posData) text=text..ztext end - text=text.." of the carrier." + text=text.." the carrier." return text end @@ -3868,7 +3889,7 @@ function AIRBOSS:_AbortPattern(playerData, X, Z, posData) local toofartext=self:_TooFarOutText(X, Z, posData) -- Send message to player. - self:_SendMessageToPlayer(toofartext.." Depart and re-enter!", 15, playerData, true) + self:_SendMessageToPlayer(toofartext.." Depart and re-enter!", 15, playerData, false) -- 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)) @@ -4092,6 +4113,8 @@ function AIRBOSS:_Debrief(playerData) -- New approach. if playerData.boltered or playerData.waveoff or playerData.patternwo then + -- TODO: can become nil when I crashed and changed to observer. + -- Get heading and distance to register zone ~3 NM astern. local heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) local distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) @@ -4199,12 +4222,12 @@ function AIRBOSS:_SendMessageToPlayer(message, duration, playerData, clear, send end --- Send text message to player client. --- Message format will be " MESSAGE". +-- Message format will be "SENDER: RECCEIVER, MESSAGE". -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -- @param #string message The message to send. -- @param #string sender The person who sends the message or nil. --- @param #string receiver The person who receives the message. Default player's onboard number. Set to \"\" for no receiver. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. -- @param #number duration Display message duration. Default 10 seconds. -- @param #boolean clear If true, clear screen from previous messages. -- @param #number delay Delay in seconds, before the message is displayed. @@ -4237,6 +4260,20 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration end +--- Check if aircraft is capable of landing on an aircraft carrier. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.) +-- @return #boolean If true, aircraft can land on a carrier. +function AIRBOSS:_IsCarrierAircraft(unit) + local carrieraircraft=false + local aircrafttype=unit:GetTypeName() + for _,actype in pairs(AIRBOSS.AircraftCarrier) do + if actype==aircrafttype then + return true + end + end + return false +end --- Checks if a group has a human player. -- @param #AIRBOSS self @@ -4384,40 +4421,41 @@ function AIRBOSS:_AddF10Commands(_unitName) local playerData=self.players[playername] -- F10/Airboss/ - local _rootPath = missionCommands.addSubMenuForGroup(_gid, self.alias, AIRBOSS.MenuF10[_gid]) + local _rootPath=missionCommands.addSubMenuForGroup(_gid, self.alias, AIRBOSS.MenuF10[_gid]) -- F10/Airboss//Results - local _statsPath = missionCommands.addSubMenuForGroup(_gid, "LSO Grades", _rootPath) + local _statsPath=missionCommands.addSubMenuForGroup(_gid, "Results", _rootPath) -- F10/Airboss//My Settings/Skil Level - local _skillPath = missionCommands.addSubMenuForGroup(_gid, "Skill Level", _rootPath) + local _skillPath=missionCommands.addSubMenuForGroup(_gid, "Skill Level", _rootPath) -- F10/Airboss//My Settings/Kneeboard - local _kneeboardPath = missionCommands.addSubMenuForGroup(_gid, "Kneeboard", _rootPath) + local _kneeboardPath=missionCommands.addSubMenuForGroup(_gid, "Kneeboard", _rootPath) - -- F10/Airboss//LSO Grades/ + -- F10/Airboss//Results/ missionCommands.addCommandForGroup(_gid, "Greenie Board", _statsPath, self._DisplayScoreBoard, self, _unitName) - missionCommands.addCommandForGroup(_gid, "My Grades", _statsPath, self._DisplayPlayerGrades, self, _unitName) + missionCommands.addCommandForGroup(_gid, "My LSO Grades", _statsPath, self._DisplayPlayerGrades, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Last Debrief", _statsPath, self._DisplayDebriefing, self, _unitName) --missionCommands.addCommandForGroup(_gid, "(Clear ALL Results)", _statsPath, self._ResetRangeStats, self, _unitName) -- F10/Airboss//Skill Level - missionCommands.addCommandForGroup(_gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) - missionCommands.addCommandForGroup(_gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) - missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) + missionCommands.addCommandForGroup(_gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) + missionCommands.addCommandForGroup(_gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) + missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) -- F10/Airboss//Kneeboard missionCommands.addCommandForGroup(_gid, "Carrier Info", _kneeboardPath, self._DisplayCarrierInfo, self, _unitName) missionCommands.addCommandForGroup(_gid, "Weather Report", _kneeboardPath, self._DisplayCarrierWeather, self, _unitName) missionCommands.addCommandForGroup(_gid, "My Status", _kneeboardPath, self._DisplayPlayerStatus, self, _unitName) missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _kneeboardPath, self._AttitudeMonitor, self, playername) - missionCommands.addCommandForGroup(_gid, "Smoke Marshal Zone", _kneeboardPath, self._SmokeMarshalZone, self, _unitName) - + missionCommands.addCommandForGroup(_gid, "Smoke Marshal Zone", _kneeboardPath, self._SmokeMarshalZone, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Flare Marshal Zone", _kneeboardPath, self._FlareMarshalZone, self, _unitName) + -- F10/Airboss// - missionCommands.addCommandForGroup(_gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Commencing!", _rootPath, self._RequestStraightIn, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Set Section", _rootPath, self._SetSection, self, _unitName) - --TODO: request refueling if recovery tanker set! make refuelling queue. add refuelling step. + missionCommands.addCommandForGroup(_gid, "Request Marshal?", _rootPath, self._RequestMarshal, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Commencing!", _rootPath, self._RequestCommence, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Request Refueling?", _rootPath, self._RequestRefueling, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Set Section!", _rootPath, self._SetSection, self, _unitName) end else @@ -4429,226 +4467,9 @@ function AIRBOSS:_AddF10Commands(_unitName) end ---- Player requests refueling. --- @param #AIRBOSS self --- @param #string _unitName Name of the player unit. -function AIRBOSS:_RequestRefueling(_unitName) - - -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - - -- Check if we have a unit which is a player. - if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData - - if playerData then - - local text="Player requested refueling." - MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) - - end - end -end - ---- Smoke current marshal zone of player. --- @param #AIRBOSS self --- @param #string _unitName Name of the player unit. -function AIRBOSS:_SmokeMarshalZone(_unitName) - - -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - - -- Check if we have a unit which is a player. - if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData - - if playerData then - - -- Get current holding zone. - local zone=self:_GetHoldingZone(playerData) - - local text="No marshal zone to smoke!" - if zone then - text="Smoking marshal zone with GREEN smoke." - zone:SmokeZone(SMOKECOLOR.Green) - end - MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) - end - end - -end - ---- Display player status. --- @param #AIRBOSS self --- @param #string _unitName Name of the player unit. -function AIRBOSS:_DisplayPlayerStatus(_unitName) - - -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - - -- Check if we have a unit which is a player. - if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData - - if playerData then - - -- Player data. - local text=string.format("Status of player %s (%s)\n", playerData.name, playerData.callsign) - text=text..string.format("--------------------------------------\n") - text=text..string.format("Current step: %s\n", playerData.step) - text=text..string.format("Skil level: %s\n", playerData.difficulty) - text=text..string.format("Aircraft: %s\n", playerData.actype) - text=text..string.format("Board number: %s\n", playerData.onboard) - text=text..string.format("Fuel: %.1f %%\n", playerData.unit:GetFuel()*100) - text=text..string.format("Group name: %s\n", playerData.group:GetName()) - text=text..string.format("# units: %s\n", #playerData.group:GetUnits()) - text=text..string.format("Section Lead: %s\n", tostring(playerData.seclead)) - - -- Flight data (if available). - local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) - if flight then - local stack=flight.flag:Get() - local stackalt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) - text=text..string.format("Aircraft: %s\n", flight.actype) - text=text..string.format("Flag/stack: %d\n", stack) - text=text..string.format("Stack alt: %d ft\n", stackalt) - text=text..string.format("# units: %s\n", flight.nunits) - text=text..string.format("# section: %s", #flight.section) - for _,_sec in pairs(flight.section) do - local sec=_sec --#AIRBOSS.Flightitem - text=text..string.format("\n- %s", sec.player.name) - end - else - text=text..string.format("Your flight is not registered in CCA.") - end - - if playerData.step==AIRBOSS.PatternStep.INITIAL then - local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) - local flydist=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) - local brc=self:_BaseRecoveryCourse() - text=text..string.format("Fly heading %03d° for %d NM and turn to BRC %03d°.", flyhdg, flydist, brc) - end - - MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) - end - end - -end - ---- Set all flights within 200 meters to be part of my section. --- @param #AIRBOSS self --- @param #string _unitName Name of the player unit. -function AIRBOSS:_SetSection(_unitName) - - -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - - -- Check if we have a unit which is a player. - if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData - - if playerData then - - -- Coordinate of flight lead. - local mycoord=_unit:GetCoordinate() - - -- Flight group. - local myflight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) - - for _,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.Flightitem - - -- Only human flight groups excluding myself. - if flight.ai==false and flight.player and flight.groupname~=myflight.groupname then - - -- Distance to other group. - local distance=flight.group:GetCoordinate():Get2DDistance(mycoord) - - if distance<200 then - table.insert(myflight.section, flight) - end - - end - end - - local text - if #myflight.section>0 then - text=string.format("Registered flight section") - text=text..string.format("- %s (lead)", myflight.player.name) - for _,_flight in paris(myflight.section) do - local flight=_flight --#AIRBOSS.Flightitem - text=text..string.format("- %s", flight.player.name) - end - else - text="No other human flights found within radius of 200 meter radius!" - end - MESSAGE:New(text, 10, "MARSHALL"):ToAll() - - end - end - -end - ---- Request straight in approach. --- @param #AIRBOSS self --- @param #string _unitName Name fo the player unit. -function AIRBOSS:_RequestStraightIn(_unitName) - self:F(_unitName) - - -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - - -- Check if we have a unit which is a player. - if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData - - if playerData then - - -- Get flight group. - local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) - - local text - if flight then - - -- Get stack value. - local stack=flight.flag:Get() - - if stack>1 then - -- We are in a higher stack. - text="Negative ghostrider, it's not your turn yet!" - else - - -- Number of aircraft currently in pattern. - local _,npattern=self:_GetQueueInfo(self.Qpattern) - - -- TODO: set nmax for pattern. Should be ~6 but let's make this 4. - if npattern>0 then - -- Patern is full! - text=string.format("Negative ghostrider, pattern is full! There are %d aircraft currently in pattern.", npattern) - else - -- Positive response. - text="You are cleared for pattern. Proceed to initial." - - -- Set player step. - playerData.step=AIRBOSS.PatternStep.COMMENCING - - -- Collaps marshal stack. - self:_CollapseMarshalStack(flight.case) - end - - end - - else - -- This flight is not yet registered! - text="Negative ghostrider, you are not yet registered inside the CCA yet!" - -- TODO: fly 10 km towards the carrier advice for skill "Flight Student" - end - - -- Send message. - self:MessageToPlayer(playerData, text, "AIRBOSS", "", 5) - end - end -end +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ROOT MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Request marshal. -- @param #AIRBOSS self @@ -4679,10 +4500,10 @@ function AIRBOSS:_RequestMarshal(_unitName) end end ---- Display last debriefing. +--- Request to commence approach. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. -function AIRBOSS:_DisplayPlayerGrades(_unitName) +function AIRBOSS:_RequestCommence(_unitName) self:F(_unitName) -- Get player unit and name. @@ -4693,76 +4514,167 @@ function AIRBOSS:_DisplayPlayerGrades(_unitName) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then + + -- Get flight group. + local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) + + local text + if flight then + + -- Get stack value. + local stack=flight.flag:Get() + + if stack>1 then + -- We are in a higher stack. + text="Negative ghostrider, it's not your turn yet!" + else + + -- Number of aircraft currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + -- TODO: set nmax for pattern. Should be ~6 but let's make this 4. + if npattern>0 then + -- Patern is full! + text=string.format("Negative ghostrider, pattern is full! There are %d aircraft currently in pattern.", npattern) + else + -- Positive response. + text="You are cleared for pattern. Proceed to initial." + + -- Set player step. + playerData.step=AIRBOSS.PatternStep.COMMENCING + + -- Collaps marshal stack. + self:_CollapseMarshalStack(flight) + end - -- Debriefing text. - local text=string.format("Debriefing:") - - -- Check if data is present. - if #playerData.debrief>0 then - text=text..string.format("\n================================\n") - for _,_data in pairs(playerData.debrief) do - local step=_data.step - local comment=_data.hint - text=text..string.format("* %s:\n",step) - text=text..string.format("%s\n", comment) end - else - text=text.." Nothing to show yet." - end - - -- Send debrief message to player - self:MessageToPlayer(playerData, text, nil , "", 30, true) - - end - end -end - - ---- Display top 10 player scores. --- @param #AIRBOSS self --- @param #string _unitName Name fo the player unit. -function AIRBOSS:_DisplayPlayerGrades(_unitName) - self:F(_unitName) - - -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - - -- Check if we have a unit which is a player. - if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData - - if playerData then - - -- Grades of player: - local text=string.format("Your grades, %s:", _playername) - - local p=0 - for i,_grade in pairs(playerData.grades) do - local grade=_grade --#AIRBOSS.LSOgrade - text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, grade.points, grade.details) - p=p+grade.points - end - - -- Number of grades. - local n=#playerData.grades - - if n>0 then - text=text..string.format("\nAverage points = %.1f", p/n) else - text=text..string.format("\nNo data available.") + -- This flight is not yet registered! + text="Negative ghostrider, you are not yet registered inside the CCA yet!" + -- TODO: fly 10 km towards the carrier advice for skill "Flight Student" end - --env.info("FF:\n"..text) + env.info(text) -- Send message. - if playerData.client then - MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) - end + self:MessageToPlayer(playerData, text, "AIRBOSS", "", 5) end end end +--- Player requests refueling. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_RequestRefueling(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + local text + if self.tanker then + + --TODO: request refueling if recovery tanker set! make refuelling queue. add refuelling step. + text="Player requested refueling. (not implemented yet)" + + -- Flight group. + local myflight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) + + if myflight then + + if self.tanker:IsRunning() then + text="Proceed to tanker at angels 6." + -- TODO: collaple stack. check in which queues flight is. + elseif self.tanker:IsReturning() then + text="Tanker is currently returning to carrier. Request denied!" + end + + else + text="You are not registered in CCA zone yet." + end + else + text="No refueling tanker available!" + end + + -- Send message. + MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) + end + end +end + +--- Set all flights within 200 meters to be part of my section. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_SetSection(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Coordinate of flight lead. + local mycoord=_unit:GetCoordinate() + + -- Flight group. + local myflight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) + + -- TODO: Only allow set section, if player is not in marshal stack yet. + + local text + if myflight then + + -- Loop over all registered flights. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.Flightitem + + -- Only human flight groups excluding myself. + if flight.ai==false and flight.player and flight.groupname~=myflight.groupname then + + -- Distance to other group. + local distance=flight.group:GetCoordinate():Get2DDistance(mycoord) + + if distance<200 then + table.insert(myflight.section, flight) + end + + end + end + + -- Info on section members. + if #myflight.section>0 then + text=string.format("Registered flight section") + text=text..string.format("- %s (lead)", myflight.player.name) + for _,_flight in paris(myflight.section) do + local flight=_flight --#AIRBOSS.Flightitem + text=text..string.format("- %s", flight.player.name) + end + else + text="No other human flights found within radius of 200 meter radius!" + end + + else + text="You are not registered in CCA zone yet." + end + + MESSAGE:New(text, 10, "MARSHALL"):ToClient(playerData.callsign) + end + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RESULTS MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Display top 10 player scores. -- @param #AIRBOSS self @@ -4815,6 +4727,114 @@ function AIRBOSS:_DisplayScoreBoard(_unitName) end end +--- Display top 10 player scores. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayPlayerGrades(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Grades of player: + local text=string.format("Your grades, %s:", _playername) + + local p=0 + for i,_grade in pairs(playerData.grades) do + local grade=_grade --#AIRBOSS.LSOgrade + + text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, grade.points, grade.details) + p=p+grade.points + end + + -- Number of grades. + local n=#playerData.grades + + if n>0 then + text=text..string.format("\nAverage points = %.1f", p/n) + else + text=text..string.format("\nNo data available.") + end + + --env.info("FF:\n"..text) + + -- Send message. + if playerData.client then + MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) + end + end + end +end + +--- Display last debriefing. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayDebriefing(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Debriefing text. + local text=string.format("Debriefing:") + + -- Check if data is present. + if #playerData.debrief>0 then + text=text..string.format("\n================================\n") + for _,_data in pairs(playerData.debrief) do + local step=_data.step + local comment=_data.hint + text=text..string.format("* %s:\n",step) + text=text..string.format("%s\n", comment) + end + else + text=text.." Nothing to show yet." + end + + -- Send debrief message to player + self:MessageToPlayer(playerData, text, nil , "", 30, true) + + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- SKIL LEVEL MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set difficulty level. +-- @param #AIRBOSS self +-- @param #string playername Player name. +-- @param #AIRBOSS.Difficulty difficulty Difficulty level. +function AIRBOSS:_SetDifficulty(playername, difficulty) + self:E({difficulty=difficulty, playername=playername}) + + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.difficulty=difficulty + local text=string.format("your difficulty level is now: %s.", difficulty) + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + else + self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- KNEEBOARD MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Turn player's aircraft attitude display on or off. -- @param #AIRBOSS self @@ -4829,24 +4849,6 @@ function AIRBOSS:_AttitudeMonitor(playername) end end ---- Set difficulty level. --- @param #AIRBOSS self --- @param #string playername Player name. --- @param #AIRBOSS.Difficulty difficulty Difficulty level. -function AIRBOSS:_SetDifficulty(playername, difficulty) - self:E({difficulty=difficulty, playername=playername}) - - local playerData=self.players[playername] --#AIRBOSS.PlayerData - - if playerData then - playerData.difficulty=difficulty - local text=string.format("Your difficulty level is now: %s.", difficulty) - self:_SendMessageToPlayer(text, 5, playerData) - self:MessageToPlayer(playerData, text, nil, "", 5) - else - self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) - end -end --- Report information about carrier. -- @param #AIRBOSS self @@ -4884,6 +4886,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) -- Message text. local text=string.format("%s info:\n", self.alias) + text=text..string.format("Carrier state %s\n", self:GetState()) text=text..string.format("Case %d Recovery\n", self.case) text=text..string.format("BRC %03d°\n", self:_BaseRecoveryCourse()) text=text..string.format("FB %03d°\n", self:_FinalBearing()) @@ -4967,6 +4970,121 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) end end + + +--- Display player status. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_DisplayPlayerStatus(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Player data. + local text=string.format("Status of player %s (%s)\n", playerData.name, playerData.callsign) + text=text..string.format("======================================================\n") + text=text..string.format("Current step: %s\n", playerData.step) + text=text..string.format("Skil level: %s\n", playerData.difficulty) + text=text..string.format("Aircraft: %s\n", playerData.actype) + text=text..string.format("Board number: %s\n", playerData.onboard) + text=text..string.format("Fuel: %.1f %%\n", playerData.unit:GetFuel()*100) + text=text..string.format("Group: %s\n", playerData.group:GetName()) + text=text..string.format("# units: %s\n", #playerData.group:GetUnits()) + text=text..string.format("Section Lead: %s\n", tostring(playerData.seclead)) + + -- Flight data (if available). + local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) + if flight then + local stack=flight.flag:Get() + local stackalt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) + text=text..string.format("Aircraft: %s\n", flight.actype) + text=text..string.format("Flag/stack: %d\n", stack) + text=text..string.format("Stack alt: %d ft\n", stackalt) + text=text..string.format("# units: %s\n", flight.nunits) + text=text..string.format("# section: %s", #flight.section) + for _,_sec in pairs(flight.section) do + local sec=_sec --#AIRBOSS.Flightitem + text=text..string.format("\n- %s", sec.player.name) + end + else + text=text..string.format("Your flight is not registered in CCA.") + end + + if playerData.step==AIRBOSS.PatternStep.INITIAL then + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate())) + local brc=self:_BaseRecoveryCourse() + text=text..string.format("\nFly heading %03d° for %.1f NM and turn to BRC %03d°.", flyhdg, flydist, brc) + end + + MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) + end + end + +end + +--- Smoke current marshal zone of player. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_SmokeMarshalZone(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Get current holding zone. + local zone=self:_GetHoldingZone(playerData) + + local text="No marshal zone to smoke!" + if zone then + text="Smoking marshal zone with GREEN smoke." + zone:SmokeZone(SMOKECOLOR.Green) + end + MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) + end + end + +end + +--- Flare current marshal zone of player. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_FlareMarshalZone(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Get current holding zone. + local zone=self:_GetHoldingZone(playerData) + + local text="No marshal zone to flare!" + if zone then + text="Flaring marshal zone with GREEN flares." + zone:FlareZone(FLARECOLOR.Green, 90) + end + MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) + end + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ From b4c82d0aacee5b3c3954af201c4c4846c5989c71 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 22 Nov 2018 16:12:00 +0100 Subject: [PATCH 053/147] docs --- Moose Development/Moose/Ops/Airboss.lua | 35 +++++--- .../Moose/Ops/RecoveryTanker.lua | 64 ++++++++++++++- Moose Development/Moose/Ops/RescueHelo.lua | 81 ++++++++++++++++++- 3 files changed, 162 insertions(+), 18 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 8e0c11213..16d4a71e3 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -73,7 +73,7 @@ -- @field #table recoverytime List of time intervals when aircraft are recovered. -- @extends Core.Fsm#FSM ---- Practice Carrier Landings +--- The boss! -- -- === -- @@ -81,7 +81,18 @@ -- -- # The AIRBOSS Concept -- --- bla bla +-- On an aircraft carrier, the AIRBOSS is guy who is in charge! +-- +-- # Recovery Cases +-- +-- The AIRBOSS class supports all three commonly used recovery cases, i.e. +-- * CASE I, which is for daytime and good weather +-- * CASE II, for daytime but poor visibility conditions and +-- * CASE III for nighttime recoveries. +-- +-- ## CASE I +-- +-- When CASE I recovery is active, -- -- @field #AIRBOSS AIRBOSS = { @@ -189,7 +200,6 @@ AIRBOSS.CarrierType={ -- @type AIRBOSS.AircraftParameters -- @field #number AoA Onspeed Angle of Attack. -- @field #number Dboat Ideal distance to the carrier. --- @field #number --- Pattern steps. @@ -436,23 +446,18 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.2" +AIRBOSS.version="0.3.2w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Set case II and III times. --- TODO: Get an _OK_ pass if long in groove. Possible other pattern wave offs as well?! -- TODO: Add radio transmission queue for LSO and airboss. -- TODO: Get correct wire when trapped. -- TODO: Add radio check (LSO, AIRBOSS) to F10 radio menu. --- DONE: Monitor holding of players/AI in zoneHolding. -- TODO: Right pattern step after bolter/wo/patternWO? -- TODO: Handle crash event. Delete A/C from queue, send rescue helo, stop carrier? --- TODO: Add aircraft numbers in queue to carrier info F10 radio output. --- DONE: Transmission via radio. --- DONE: Get board numbers. -- TODO: Get fuel state in pounds. -- TODO: Add user functions. -- TODO: Generalize parameters for other carriers. @@ -462,6 +467,11 @@ AIRBOSS.version="0.3.2" -- TODO: Foul deck check. -- TODO: Persistence of results. -- TODO: Strike group with helo bringing cargo etc. +-- DONE: Add aircraft numbers in queue to carrier info F10 radio output. +-- DONE: Monitor holding of players/AI in zoneHolding. +-- DONE: Transmission via radio. +-- DONE: Get board numbers. +-- DONE: Get an _OK_ pass if long in groove. Possible other pattern wave offs as well?! -- DONE: Add scoring to radio menu. -- DONE: Optimized debrief. -- DONE: Add automatic grading. @@ -1148,9 +1158,9 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC - local dist local alt local aoa + local dist local speed if step==AIRBOSS.PatternStep.DESCENT4K then @@ -1257,6 +1267,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) end + return alt, aoa, dist, speed end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1546,8 +1557,6 @@ function AIRBOSS:_RemoveFlightGroup(group) end end - - --- Orbit at a specified position at a specified alititude with a specified speed. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. @@ -2245,7 +2254,7 @@ function AIRBOSS:OnEventLand(EventData) -- AI: Decrease number of units in flight and remove group from pattern queue if all units landed. if self:_InQueue(self.Qpattern, EventData.IniGroup) then self:_RemoveQueue(self.Qpattern, EventData.IniGroup) - end + end end diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 4e4867c60..683a821b6 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -49,7 +49,65 @@ -- -- # Recovery Tanker -- --- bla bla +-- A recovery tanker acts as refueling unit flying overhead an aircraft carrier in order to supply incoming flights with gas if necessary. +-- +-- # Simple Script +-- +-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named "USS Stennis". +-- +-- Secondly, you need to define a recovery tanker group in the mission editor and set it to "LATE ACTIVATED". The name of the group we'll use is "Texaco". +-- +-- The basic script is very simple and consists of only two lines. +-- +-- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") +-- TexacoStennis:Start() +-- +-- The first line will create a new RECOVERYTANKER object and the second line starts the process. +-- +-- With this setup, the tanker will be spawned on the USS Stennis with running engines. After it takes off, it will fly a position astern of the boat and from there start its +-- pattern. This is a counter clockwise racetrack pattern at angels 6. +-- +-- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Pattern.jpg) +-- +-- The "downwind" leg of the pattern is normally used for refueling. +-- +-- Once the tanker runs out of fuel itself, it will return to the carrier and be respawned. +-- +-- # Fine Tuning +-- +-- Several parameters can be customized by the mission designer. +-- +-- ## Adjusting the Takeoff Type +-- +-- By default, the tanker is spawned with running engies on the carrier. The mission designer has set option to set the take off type via the @{#RECOVERYTANKER.SetTakeoff} function. +-- Or via shortcuts +-- +-- * @{#RECOVERYTANKER.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default. +-- * @{#RECOVERYTANKER.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off. +-- * @{#RECOVERYTANKER.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the tanker will be spawned in air relatively far behind the carrier. +-- +-- For example, +-- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") +-- TexacoStennis:SetTakeoffAir() +-- TexacoStennis:Start() +-- will spawn the tanker several nautical miles astern the carrier. From there it will start its pattern. +-- +-- Spawning in air is not as realsitic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck. +-- +-- **Note** that when spawning in air is set, the tanker will also not return to the boat, once it is out of fuel. Instead it will be respawned directly in air. +-- +-- If only the first spawning should happen on the carrier, one use the @{#RECOVERYTANKER.SetRespawnInAir}() function to command that all subsequent spawning +-- will happen in air. +-- +-- If the helo should no be respawned at all, one can set @{#RECOVERYTANKER.SetRespawnOff}(). +-- +-- ## Adjusting the Pattern +-- +-- The racetrack pattern parameters can be fine tuned via the following functions: +-- +-- * @{#RECOVERYTANKER.SetAltitude}(*altitude*), where *altitude* is the pattern altitude in feet. Default 6000 ft. +-- * @{#RECOVERYTANKER.SetSpeed}(*speed*), where *speed* is the pattern speed in knots. Default is 272 knots. +-- * @{#RECOVERYTANKER.SetRacetrackDistances}(*distbow*, *diststern*), where *distbow* and *diststern* are the distances ahead and astern the boat, respectively. -- -- @field #RECOVERYTANKER RECOVERYTANKER = { @@ -734,8 +792,8 @@ function RECOVERYTANKER:_PatternUpdate() local wp={} -- New waypoint with orbit pattern task. - wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil , self.speed, {}, "Current Position") - wp[2]=p0:WaypointAirTurningPoint(nil, self.speed, {taskorbit}, "Tanker Orbit") + --wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil , self.speed, {}, "Current Position") + wp[1]=p0:WaypointAirTurningPoint(nil, self.speed, {taskorbit}, "Tanker Orbit") -- Initialize WP and route tanker. self.tanker:WayPointInitialize(wp) diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 0c767d3eb..93f2ce8b1 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -45,9 +45,85 @@ -- -- ![Banner Image](..\Presentations\RESCUEHELO\RescueHelo_Main.jpg) -- --- # Recue helo +-- # Recue Helo +-- +-- The rescue helo will fly in close formation with another unit, which is typically an aircraft carrier. +-- It's mission is to rescue crashed units or ejected pilots. Well, and to look cool... +-- +-- # Simple Script +-- +-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named "USS Stennis". +-- +-- Secondly, you need to define a recue helicopter group in the mission editor and set it to "LATE ACTIVATED". The name of the group we'll use is "Recue Helo". +-- +-- The basic script is very simple and consists of only two lines. +-- +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:Start() +-- +-- The first line will create a new RESCUEHELO object and the second line starts the process. +-- +-- **NOTE** that it is *very important* to define the RESCUEHELO object as **global** variable. Otherwise, the lua garbage collector will kill the formation! +-- +-- By default, the helo will be spawned on the USS Stennis with hot engines. Then it will take off and go on station on the starboard side of the boat. +-- +-- Once the helo is out of fuel, it will return to the carrier. When the helo lands, it will be respawned immidiately and go back on station. +-- +-- If a unit crashes or a pilot ejects within a radius of 100 km from the USS Stennis, the helo will automatically fly to the crash side and +-- rescue to pilot. This will take around 5 minutes. After that, the helo will return to the Stennis, land there and bring back the poor guy. +-- When this is done, the helo will go back on station. +-- +-- # Fine Tuning +-- +-- The implementation allows to customize quite a few settings easily +-- +-- ## Adjusting the Takeoff Type +-- +-- By default, the helo is spawned with running engies on the carrier. The mission designer has set option to set the take off type via the @{#RESCUEHELO.SetTakeoff} function. +-- Or via shortcuts +-- +-- * @{#RESCUEHELO.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default. +-- * @{#RESCUEHELO.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off. +-- * @{#RESCUEHELO.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the helo will be spawned in air near the unit which he follows. +-- +-- For example, +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:SetTakeoffAir() +-- RescueheloStennis:Start() +-- will spawn the helo near the USS Stennis in air. +-- +-- Spawning in air is not as realsitic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck. +-- +-- **Note** that when spawning in air is set, the helo will also not return to the boat, once it is out of fuel. Instead it will be respawned in air. +-- +-- If only the first spawning should happen on the carrier, one use the @{#RESCUEHELO.SetRespawnInAir}() function to command that all subsequent spawning +-- will happen in air. +-- +-- If the helo should no be respawned at all, one can set @{#RESCUEHELO.SetRespawnOff}(). +-- +-- ## Setting a Home Base +-- +-- It is possible to define a "home base" other than the aircaft carrier. For example, one could imagine a strike group, and the helo will be spawned from +-- another ship which has a helo pad. +-- +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:SetHomeBase(AIRBASE:FindByName("USS Normandy")) +-- RescueheloStennis:Start() +-- +-- In this case, the helo will be spawned on the USS Normandy and then make its way to the USS Stennis to establish the formation. +-- Note that the distance to the mother ship should be rather small since the helo will go there very slowly. +-- +-- Once the helo runs out of fuel, it will return to the USS Normandy and not the Stennis for respawning. +-- +-- +-- # Adjusting the Formation Positon +-- +-- The position of the helo relative to the mother ship can be tuned via the functions +-- +-- * @{#RESCUEHELO.SetAltitude}(*altitude*), where *altitude* is the altitude the helo flies at in meters. Default is 70 meters. +-- * @{#RESCUEHELO.SetOffsetX}(*distance*)}, where *distance is the distance in the direction of movement of the carrier. Default is 200 meters. +-- * @{#RESCUEHELO.SetOffsetZ}(*distance*)}, where *distance is the distance on the starboard side. Default is 200 meters. -- --- bla bla -- -- @field #RESCUEHELO RESCUEHELO = { @@ -80,6 +156,7 @@ RESCUEHELO.version="0.9.3" -- TODO: Add option to stop carrier while rescue operation is in progress. -- TODO: Possibility to add already present/spawned aircraft, e.g. for warehouse. +-- TODO: Add option to deactivate the rescueing. -- TODO: Write documenation. -- DONE: Add rescue event when aircraft crashes. -- DONE: Make offset input parameter. From 9269e1e943df9a3f4e5999b84eb9ae0758e109ce Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 22 Nov 2018 23:27:43 +0100 Subject: [PATCH 054/147] AIBOSS v0.3.3 little bug fixes --- Moose Development/Moose/Ops/Airboss.lua | 38 ++++++++++++++++++------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 16d4a71e3..544e0b5a2 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -446,7 +446,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.2w" +AIRBOSS.version="0.3.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -558,7 +558,7 @@ function AIRBOSS:New(carriername, alias) self:SetCarrierControlledZone() -- Default recovery case. - self:SetRecoveryCase(1) + self:SetRecoveryCase(3) -- Init default sound files. for _name,_sound in pairs(AIRBOSS.Soundfile) do @@ -2565,6 +2565,10 @@ function AIRBOSS:_Descent4k(playerData) -- Get optimal altitude, distance and speed. local altitude=self:_GetAircraftParameters(playerData) + + -- TODO: only speed is checked here! + + MESSAGE:New("Descent 4k step reached", 5):ToAllIf(self.Debug) -- Get altitude. local hint, debrief=self:_AltitudeCheck(playerData, altitude) @@ -2597,8 +2601,12 @@ function AIRBOSS:_Platform(playerData) -- Check if we are in front of the boat (diffX > 0). if self:_CheckLimits(X, Z, self.Platform) then + MESSAGE:New("Platform step reached", 5):ToAllIf(self.Debug) + -- Get optimal altitiude. local altitude=self:_GetAircraftParameters(playerData) + + --TODO: check speed. -- Get altitude. local hint, debrief=self:_AltitudeCheck(playerData, altitude) @@ -2630,6 +2638,10 @@ function AIRBOSS:_DirtyUp(playerData) -- Check if we are in front of the boat (diffX > 0). if self:_CheckLimits(X, Z, self.DirtyUp) then + + MESSAGE:New("Dirty up step reached", 5):ToAllIf(self.Debug) + + --TODO: speed check -- Get optimal altitiude. local altitude=self:_GetAircraftParameters(playerData) @@ -2671,6 +2683,8 @@ function AIRBOSS:_Bullseye(playerData) -- Check that we reached the position. if self:_CheckLimits(X, Z, self.Bullseye) then + MESSAGE:New("Bullseye step reached", 5):ToAllIf(self.Debug) + -- Get optimal altitiude. local altitude=self:_GetAircraftParameters(playerData) @@ -2681,10 +2695,12 @@ function AIRBOSS:_Bullseye(playerData) self:_SendMessageToPlayer(hint, 10, playerData) -- Debrief. - self:_AddToSummary(playerData, "Bulls Eye", debrief) + self:_AddToSummary(playerData, "Bullseye", debrief) - -- Next step: Early Break. - playerData.step=AIRBOSS.PatternStep.FINAL + -- Next step: Final approach in the groove. + --playerData.step=AIRBOSS.PatternStep.FINAL + -- Next step: Groove Call the ball. + playerData.step=AIRBOSS.PatternStep.GROOVE_XX end end @@ -3977,7 +3993,7 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) -- Extend or decrease depending on skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then - hint=hint..string.format("Optimal altitude is %d ft.", UTILS.MetersToFeet(checkpoint.Altitude)) + hint=hint..string.format("Optimal altitude is %d ft.", UTILS.MetersToFeet(altopt)) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then --hint=hint.."\n" elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then @@ -3985,7 +4001,7 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) end -- Debrief text. - local debrief=string.format("Altitude %d ft = %d%% deviation from %d ft optimum.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(checkpoint.Altitude)) + local debrief=string.format("Altitude %d ft = %d%% deviation from %d ft optimum.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(altopt)) return hint, debrief end @@ -4026,7 +4042,7 @@ function AIRBOSS:_DistanceCheck(playerData, optdist) -- Extend or decrease depending on skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then - hint=hint..string.format(" Optimal distance is %d NM.", UTILS.MetersToNM(checkpoint.Distance)) + hint=hint..string.format(" Optimal distance is %d NM.", UTILS.MetersToNM(optdist)) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then --hint=hint.."\n" elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then @@ -4034,7 +4050,7 @@ function AIRBOSS:_DistanceCheck(playerData, optdist) end -- Debriefing text. - local debrief=string.format("Distance %.1f NM = %d%% deviation from %.1f NM optimum.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(checkpoint.Distance)) + local debrief=string.format("Distance %.1f NM = %d%% deviation from %.1f NM optimum.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(optdist)) return hint, debrief end @@ -4075,7 +4091,7 @@ function AIRBOSS:_AoACheck(playerData, optaoa) -- Extend or decrease depending on skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then - hint=hint..string.format(" Optimal AoA is %.1f.", checkpoint.AoA) + hint=hint..string.format(" Optimal AoA is %.1f.", optaoa) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then --hint=hint.."\n" elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then @@ -4083,7 +4099,7 @@ function AIRBOSS:_AoACheck(playerData, optaoa) end -- Debriefing text. - local debrief=string.format("AoA %.1f = %d%% deviation from %.1f optimum.", aoa, _error, checkpoint.AoA) + local debrief=string.format("AoA %.1f = %d%% deviation from %.1f optimum.", aoa, _error, optaoa) return hint, debrief end From 8133ef2036d7f22af9af6928b1cf669769d57385 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 23 Nov 2018 16:30:32 +0100 Subject: [PATCH 055/147] AIRBOSS v0.3.3w --- Moose Development/Moose/Ops/Airboss.lua | 299 +++++++++++++++----- Moose Development/Moose/Utilities/Utils.lua | 30 +- 2 files changed, 257 insertions(+), 72 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 544e0b5a2..4225b7904 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -49,6 +49,9 @@ -- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneInitial Zone usually 3 NM astern of carrier where pilots start their CASE I pattern. +-- @field Core.Zone#ZONE_UNIT zonePlatform Zone astern the carrier where pilots should hit 5000 ft in CASE II/III. +-- @field Core.Zone#ZONE_UNIT zoneDirtyup Zone astern the carrier where pilots should hit 1200 ft and dirty up. +-- @field Core.Zone#ZONE_UNIT zoneBullseye Zone astern the carrier where pilots should intercept the glide slope. -- @field #table players Table of players. -- @field #table menuadded Table of units where the F10 radio menu was added. -- @field #AIRBOSS.Checkpoint Upwind Upwind checkpoint. @@ -71,6 +74,7 @@ -- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. -- @field Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. -- @field #table recoverytime List of time intervals when aircraft are recovered. +-- @field #number holdingoffset Offset [degrees] of Case II/III holding pattern. Default 0 degrees. -- @extends Core.Fsm#FSM --- The boss! @@ -116,6 +120,9 @@ AIRBOSS = { zoneCCA = nil, zoneCCZ = nil, zoneInitial = nil, + zonePlatform = nil, + zoneDirtyup = nil, + zoneBullseye = nil, players = {}, menuadded = {}, Upwind = {}, @@ -139,6 +146,7 @@ AIRBOSS = { tanker = nil, warehouse = nil, recoverytime = {}, + holdoffset = 0, } --- Player aircraft types capable of landing on carriers. @@ -370,6 +378,7 @@ AIRBOSS.GroovePos={ -- @field #number GSE Glide slope error in degrees. -- @field #number LUE Lineup error in degrees. -- @field #number Roll Roll angle. +-- @field #number Rhdg Relative heading player to carrier. 0=parallel, +-90=perpendicular. --- LSO grade -- @type AIRBOSS.LSOgrade @@ -446,7 +455,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.3" +AIRBOSS.version="0.3.3w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -548,9 +557,14 @@ function AIRBOSS:New(carriername, alias) return nil end - -- Zone 3 NM astern and 100 m starboard of the carrier with radius of 0.5 km. + -- CASE I/II moving zone: Zone 3 NM astern and 100 m starboard of the carrier with radius of 0.5 km. self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, 0.5*1000, {dx=-UTILS.NMToMeters(3), dy=100, relative_to_unit=true}) + -- CASE II/III moving zones. + self.zonePlatform = ZONE_UNIT:New("Platform Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters(20), theta=-171, relative_to_unit=true}) + self.zoneDirtyup = ZONE_UNIT:New("Dirty Up Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters(10), theta=-171, relative_to_unit=true}) + self.zoneBullseye = ZONE_UNIT:New("Bulleye Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters( 3), theta=-171, relative_to_unit=true}) + -- CCA 50 NM radius zone around the carrier. self:SetCarrierControlledArea() @@ -1783,7 +1797,7 @@ function AIRBOSS:_AddMarshallGroup(flight, flagvalue) -- TODO: Get correct board number if possible? local boardnumber=tostring(flight.onboardnumbers[unitname]) local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(flagvalue, flight.case)) - local brc=self:_BaseRecoveryCourse() + local brc=self:GetBRC() -- Marshal message. -- TODO: Get charlie time estimate. @@ -2598,8 +2612,11 @@ function AIRBOSS:_Platform(playerData) return end - -- Check if we are in front of the boat (diffX > 0). - if self:_CheckLimits(X, Z, self.Platform) then + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self.zonePlatform) + + -- Check if we are in zone. + if inzone then MESSAGE:New("Platform step reached", 5):ToAllIf(self.Debug) @@ -2636,8 +2653,11 @@ function AIRBOSS:_DirtyUp(playerData) return end - -- Check if we are in front of the boat (diffX > 0). - if self:_CheckLimits(X, Z, self.DirtyUp) then + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self.zoneDirtyup) + + --if self:_CheckLimits(X, Z, self.DirtyUp) then + if inzone then MESSAGE:New("Dirty up step reached", 5):ToAllIf(self.Debug) @@ -2679,10 +2699,14 @@ function AIRBOSS:_Bullseye(playerData) self:_AbortPattern(playerData, X, Z, self.Bullseye) return end + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self.zoneBullseye) -- Check that we reached the position. - if self:_CheckLimits(X, Z, self.Bullseye) then - + --if self:_CheckLimits(X, Z, self.Bullseye) then + if inzone then + MESSAGE:New("Bullseye step reached", 5):ToAllIf(self.Debug) -- Get optimal altitiude. @@ -2797,16 +2821,9 @@ function AIRBOSS:_CheckForLongDownwind(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z=self:_GetDistances(playerData.unit) - -- Get relative heading. - local relhead=self:_GetRelativeHeading(playerData.unit) - -- One NM from carrier is too far. local limit=UTILS.NMToMeters(-1.5) - local text=string.format("Long groove check: X=%d, relhead=%.1f", X, relhead) - self:T(text) - --MESSAGE:New(text, 1):ToAllIf(self.Debug) - -- Check we are not too far out w.r.t back of the boat. if X5. This would mean the player has not tunred in correctly! -- Groove playerData.groove.X0=groovedata @@ -3039,12 +3065,10 @@ function AIRBOSS:_Groove(playerData) end -- Lineup with runway centerline. - local lineup=self:_Lineup(playerData) - local lineupError=lineup-self.carrierparam.rwyangle + local lineupError=self:_Lineup(playerData, true) -- Glide slope. - local glideslope=self:_Glideslope(playerData) - local glideslopeError=glideslope-3.5 --TODO: maybe 3.0? + local glideslopeError=self:_Glideslope(playerData, 3.5) -- Get AoA. local AoA=playerData.unit:GetAoA() @@ -3064,6 +3088,7 @@ function AIRBOSS:_Groove(playerData) groovedata.GSE=glideslopeError groovedata.LUE=lineupError groovedata.Roll=playerData.unit:GetRoll() + groovedata.Rhdg=self:_GetRelativeHeading(playerData.unit, true) if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX then @@ -3316,8 +3341,8 @@ function AIRBOSS:_DetailedPlayerStatus(playerData) playerData.step==AIRBOSS.PatternStep.GROOVE_IC or playerData.step==AIRBOSS.PatternStep.GROOVE_AR or playerData.step==AIRBOSS.PatternStep.GROOVE_IW then - local lineup=self:_Lineup(playerData)-self.carrierparam.rwyangle - local glideslope=self:_Glideslope(playerData)-3.5 + local lineup=self:_Lineup(playerData, true) + local glideslope=self:_Glideslope(playerData, 3.5) text=text..string.format("\nLU Error = %.1f° (line up)", lineup) text=text..string.format("\nGS Error = %.1f° (glide slope)", glideslope) end @@ -3331,8 +3356,12 @@ end --- Get glide slope of aircraft. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. --- @return #number Glide slope angle in degrees measured from the -function AIRBOSS:_Glideslope(playerData) +-- @pram #number gangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope. +-- @return #number Glide slope angle in degrees measured from the deck of the carrier and third wire. +function AIRBOSS:_Glideslope(playerData, gangle) + + -- Default is 0. + gangle=gangle or 0 -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi = self:_GetDistances(playerData.unit) @@ -3342,18 +3371,19 @@ function AIRBOSS:_Glideslope(playerData) local x=math.abs(self.carrierparam.wire3-X) --TODO: Check if carrier has wires later. local glideslope=math.atan(h/x) - return math.deg(glideslope) + return math.deg(glideslope)-gangle end ---- Get line up of player wrt to carrier runway. +--- Get line up of player wrt to carrier. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #boolean runway If true, include angled runway. -- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. -- @return #number Distance from carrier tail to player aircraft in meters. -function AIRBOSS:_Lineup(playerData) +function AIRBOSS:_Lineup(playerData, runway) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi = self:_GetDistances(playerData.unit) + local X, Z, rho, phi = self:_GetDistances(playerData.unit) -- Position at the end of the deck. From there we calculate the angle. local b={x=self.carrierparam.sterndist, z=0} @@ -3365,51 +3395,61 @@ function AIRBOSS:_Lineup(playerData) local c={x=b.x-a.x, y=0, z=b.z-a.z} -- Current line up and error wrt to final heading of the runway. - local lineup=math.atan2(c.z, c.x) + local lineup=math.det(math.atan2(c.z, c.x)) + + -- Include runway. + if runway then + lineup=lineup-self.carrierparam.rwyangle + end return math.deg(lineup), UTILS.VecNorm(c) end ---- Get base recovery course (BRC) of carrier. +--- Get true (or magnetic) heading of carrier. -- @param #AIRBOSS self --- @param #boolean True If true, return true bearing. Otherwise (default) return magnetic bearing. --- @return #number BRC in degrees. -function AIRBOSS:_BaseRecoveryCourse(True) - self:E({TrueBearing=True}) - - -- Current true heading of carrier. - local hdg=self.carrier:GetHeading() +-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned. +-- @return #number Carrier heading in degrees. +function AIRBOSS:GetHeading(magnetic) + self:F3({magnetic=magnetic}) - -- Final (true) bearing. - local brc=hdg + -- Carrier heading + local hdg=self.carrier:GetHeading() - -- Magnetic bearing. - if True==false then - --TODO: Conversion to magnetic, i.e. include magnetic declination of current map. + -- Include magnetic declination. + if magnetic then + hdg=hdg-UTILS.GetMagneticDeclination() end -- Adjust negative values. - if brc<0 then - brc=brc+360 - end + if hdg<0 then + hdg=hdg+360 + end - return brc + return hdg +end + +--- Get base recovery course (BRC) of carrier. +-- The is the magnetic heading of the carrier. +-- @param #AIRBOSS self +-- @return #number BRC in degrees. +function AIRBOSS:GetBRC() + return self:GetHeading(true) end --- Get final bearing (FB) of carrier. -- By default, the routine returns the magnetic FB depending on the current map (Caucasus, NTTR, Normandy, Persion Gulf etc). --- The true bearing can be obtained by setting the *True* parameter to true. +-- The true bearing can be obtained by setting the *TrueNorth* parameter to true. -- @param #AIRBOSS self --- @param #boolean True If true, return true bearing. Otherwise (default) return magnetic bearing. +-- @param #boolean magnetic If true, magnetic FB is returned. -- @return #number FB in degrees. -function AIRBOSS:_FinalBearing(True) +function AIRBOSS:GetFinalBearing(magnetic) - -- Base Recovery Course of carrier. - local brc=self:_BaseRecoveryCourse(True) + -- First get the heading. + local fb=self:GetHeading(magnetic) -- Final baring = BRC including angled deck. - local fb=brc+self.carrierparam.rwyangle + fb=fb+self.carrierparam.rwyangle -- Adjust negative values. if fb<0 then @@ -3421,11 +3461,12 @@ end --- Get radial, i.e. the final bearing FB-180 degrees. -- @param #AIRBOSS self +-- @param #boolean magnetic If true, magnetic FB is returned. -- @return #number Radial in degrees. -function AIRBOSS:_Radial() +function AIRBOSS:GetRadial(magnetic) -- Get radial. - local radial=self:_FinalBearing()-180 + local radial=self:GetFinalBearing(magnetic)-180 -- Adjust for negative values. if radial<0 then @@ -3436,18 +3477,51 @@ function AIRBOSS:_Radial() end --- Get relative heading of player wrt carrier. +-- This is the angle between the direction vector of the carrier and the direction vector of the provided unit. +-- Note that this is calculated in the X-Z plane, i.e. the altitude Y is not taken into account. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Player unit. --- @return #number Relative heading in degrees. -function AIRBOSS:_GetRelativeHeading(unit) +-- @param #boolean runway (Optional) If true, return relative heading of unit wrt to angled runway of the carrier. +-- @return #number Relative heading in degrees. An angle of 0 means, unit fly parallel to carrier. An angle of + or - 90 degrees means, unit flies perpendicular to carrier. +function AIRBOSS:_GetRelativeHeading(unit, runway) + + -- Direction vector of the carrier. local vC=self.carrier:GetOrientationX() + + -- Direction vector of the unit. local vP=unit:GetOrientationX() + -- We only want the X-Z plane. Aircraft could fly parallel but ballistic and we dont want the "pitch" angle. + vC.y=0 + vP.y=0 + -- Get angle between the two orientation vectors in rad. - local relHead=math.acos(UTILS.VecDot(vC,vP)/UTILS.VecNorm(vC)/UTILS.VecNorm(vP)) + local rhdg=math.deg(math.acos(UTILS.VecDot(vC,vP)/UTILS.VecNorm(vC)/UTILS.VecNorm(vP))) + + -- Include runway angle. + if runway then + rhdg=rhdg-self.carrierparam.rwyangle + end + + -- TODO another way would be to get the heading of the carrier and the heading of the unit and calc difference? + -- Heading of unit. + local unitheading=unit:GetHeading() + + -- Heading of carrier. + local carrierheading + + -- Include runway? + if runway then + carrierheading=self:GetFinalBearing(false) + else + carrierheading=self:GetHeading(false) + end + + rhdg=unitheading-carrierheading + -- Return heading in degrees. - return math.deg(relHead) + return rhdg end --- Calculate distances between carrier and player unit. @@ -4104,6 +4178,55 @@ function AIRBOSS:_AoACheck(playerData, optaoa) return hint, debrief end +--- Evaluate player's speed. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number speedopt Optimal speed. +-- @return #string Feedback text. +-- @return #string Debriefing text. +function AIRBOSS:_SpeedCheck(playerData, speedopt) + + if speedopt==nil then + return nil, nil + end + + -- Player altitude. + local speed=playerData.unit:GetVelocityMPS() + + -- Get relative score. + local lowscore, badscore=self:_GetGoodBadScore(playerData) + + -- Altitude error +-X% + local _error=(speed-speedopt)/speedopt*100 + + local hint + if _error>badscore then + hint=string.format("You're fast.") + elseif _error>lowscore then + hint= string.format("You're slightly fast.") + elseif _error<-badscore then + hint=string.format("You're low.") + elseif _error<-lowscore then + hint=string.format("You're slightly slow.") + else + hint=string.format("Good speed.") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint..string.format(" Optimal altitude is %d ft.", UTILS.MetersToFeet(speedopt)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + --hint=hint.."\n" + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + hint="" + end + + -- Debrief text. + local debrief=string.format("Speed %d knots = %d%% deviation from %d knots optimum.", UTILS.MpsToKnots(speed), _error, UTILS.MpsToKnots(speedopt)) + + return hint, debrief +end + --- Append text to debrief text. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. @@ -4122,6 +4245,7 @@ function AIRBOSS:_Debrief(playerData) -- LSO grade, points, and flight data analyis. local grade, points, analysis=self:_LSOgrade(playerData) + -- My grade. local mygrade={} --#AIRBOSS.LSOgrade mygrade.grade=grade mygrade.points=points @@ -4133,7 +4257,7 @@ function AIRBOSS:_Debrief(playerData) -- LSO grade message. local text=string.format("%s %.1f PT - %s", grade, points, analysis) text=text..string.format("Your detailed debriefing can now be seen in F10 radio menu.") - self:MessageToPlayer(playerData,text, "LSO","" , 30, true) + self:MessageToPlayer(playerData,text, "LSO", "", 30, true) -- New approach. if playerData.boltered or playerData.waveoff or playerData.patternwo then @@ -4146,14 +4270,46 @@ function AIRBOSS:_Debrief(playerData) local text=string.format("fly heading %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) self:_SendMessageToPlayer(text, 10, playerData, false, nil, 30) + self:MessageToPlayer(playerData, text, "LSO", nil, 10) -- Next step? -- TODO: CASE I: After bolter/wo turn left and climb to 600 ft and re-enter the pattern. But do not go to initial but reenter earlier? -- TODO: CASE I: After pattern wo? go back to initial, I guess? -- TODO: CASE III: After bolter/wo turn left and climb to 1200 ft and re-enter pattern? -- TODO: CASE III: After pattern wo? No idea... - playerData.step=AIRBOSS.PatternStep.COMMENCING - end + + -- + local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flight) + + if flight then + + if flight.case==1 then + -- CASE I + if playerData.boltered or playerData.waveoff then + -- CASE I bolter or waveoff ==> stay in pattern and try again. + playerData.step=AIRBOSS.PatternStep.COMMENCING + elseif playerData.patternwo then + -- CASE I pattern wave off. + -- Ask again? Back to marshal. + playerData.step=AIRBOSS.PatternStep.COMMENCING + end + + elseif flight.case==2 then + + + + elseif flight.case==3 then + + end + + else + end + playerData.step=AIRBOSS.PatternStep.COMMENCING + + + elseif playerData.landed then + + end -- Next step. playerData.step=AIRBOSS.PatternStep.UNDEFINED @@ -4913,8 +5069,8 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) local text=string.format("%s info:\n", self.alias) text=text..string.format("Carrier state %s\n", self:GetState()) text=text..string.format("Case %d Recovery\n", self.case) - text=text..string.format("BRC %03d°\n", self:_BaseRecoveryCourse()) - text=text..string.format("FB %03d°\n", self:_FinalBearing()) + text=text..string.format("BRC %03d°\n", self:GetBRC()) + text=text..string.format("FB %03d°\n", self:GetFinalBearing(true)) text=text..string.format("Speed %d kts\n", carrierspeed) text=text..string.format("Airboss radio %.3f MHz\n", self.Carrierfreq) --TODO: add modulation text=text..string.format("LSO radio %.3f MHz\n", self.LSOfreq) @@ -5042,9 +5198,12 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) end if playerData.step==AIRBOSS.PatternStep.INITIAL then + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate())) - local brc=self:_BaseRecoveryCourse() + local brc=self:GetBRC() + + text=text..string.format("\nFly heading %03d° for %.1f NM and turn to BRC %03d°.", flyhdg, flydist, brc) end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index e9aa475ce..be2754d19 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -744,8 +744,34 @@ function UTILS.TACANToFrequency(TACANChannel, TACANMode) end ---- Returns the DCS map/theatre as optained by env.mission.theatre. --- @return #string DCS map string. +--- Returns the DCS map/theatre as optained by env.mission.theatre +-- @return #string DCS map name . function UTILS.GetDCSMap() return env.mission.theatre end + +--- Returns the magnetic declination of the map. +-- Returned values for the current maps are: +-- +-- * Caucasus +6 +-- * NTTR ? +-- * Normandy ? +-- * Persion Gulf ? +-- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre +-- @return #string Declination in degrees. +function UTILS.GetMagneticDeclination(map) + + -- Map. + map=map or UTILS.GetDCSMap() + + local declination=0 + if map=="Caucasus" then + declination=6 + else + declination=0 + end + + return declination +end + + From 5af235a345cfa9fd7fbaf8357f98404d68bf77be Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 23 Nov 2018 23:57:54 +0100 Subject: [PATCH 056/147] AIRBOSS v0.3.4 --- Moose Development/Moose/Ops/Airboss.lua | 565 ++++++++++++++---------- 1 file changed, 327 insertions(+), 238 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 4225b7904..d21fa38e2 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -455,7 +455,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.3w" +AIRBOSS.version="0.3.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -572,7 +572,7 @@ function AIRBOSS:New(carriername, alias) self:SetCarrierControlledZone() -- Default recovery case. - self:SetRecoveryCase(3) + self:SetRecoveryCase(1) -- Init default sound files. for _name,_sound in pairs(AIRBOSS.Soundfile) do @@ -1023,11 +1023,10 @@ function AIRBOSS:_InitStennis() -- 4k descent from holding pattern to 5k platform. self.Descent4k.name="Descent 4k" self.Descent4k.Xmin=-UTILS.NMToMeters(50) -- Not more than 50 NM behind the boat. - self.Descent4k.Xmax=-UTILS.NMToMeters(20) -- Not more than 20 NM closer to the boat from behind. + self.Descent4k.Xmax=nil -- -UTILS.NMToMeters(20) -- Not more than 20 NM closer to the boat from behind. self.Descent4k.Zmin=-UTILS.NMToMeters(15) -- Not more than 15 NM port/left of boat. self.Descent4k.Zmax= UTILS.NMToMeters(5) -- Not more than 5 NM starboard/right of boat. self.Descent4k.LimitXmin=nil - --TODO: better rho dist. decrease descent 20 2000 ft/min at 5000 ft alt and user rad alt. self.Descent4k.LimitXmax=-UTILS.NMToMeters(21) -- Check and next step when 21 NM behind the boat. self.Descent4k.LimitZmin=nil self.Descent4k.LimitZmax=nil @@ -1068,7 +1067,7 @@ function AIRBOSS:_InitStennis() self.Bullseye.LimitZmin=nil self.Bullseye.LimitZmax=nil - -- Upwind leg or break entry. + -- Upwind leg (break entry). self.Upwind.name="Upwind" self.Upwind.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of ?. self.Upwind.Xmax= nil @@ -1090,7 +1089,7 @@ function AIRBOSS:_InitStennis() self.BreakEarly.LimitZmin=-UTILS.NMToMeters(0.2) -- -370 m port self.BreakEarly.LimitZmax= nil - -- Late break + -- Late break. self.BreakLate.name="Late Break" self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? @@ -1101,7 +1100,7 @@ function AIRBOSS:_InitStennis() self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.8) -- -1470 m port self.BreakLate.LimitZmax= nil - -- Abeam position + -- Abeam position. self.Abeam.name="Abeam Position" self.Abeam.Xmin= nil self.Abeam.Xmax= nil @@ -1112,7 +1111,7 @@ function AIRBOSS:_InitStennis() self.Abeam.LimitZmin= nil self.Abeam.LimitZmax= nil - -- At the ninety + -- At the Ninety. self.Ninety.name="Ninety" self.Ninety.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. LIG check anyway. self.Ninety.Xmax= 0 -- Must be behind the boat. @@ -1123,7 +1122,7 @@ function AIRBOSS:_InitStennis() self.Ninety.LimitZmin=nil self.Ninety.LimitZmax=-UTILS.NMToMeters(0.6) -- Check and next step when 0.6 NM port. - -- Wake position + -- At the Wake. self.Wake.name="Wake" self.Wake.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. self.Wake.Xmax= 0 -- Must be behind the boat. @@ -1134,7 +1133,7 @@ function AIRBOSS:_InitStennis() self.Wake.LimitZmin=0 -- Check and next step when directly behind the boat. self.Wake.LimitZmax=nil - -- In the groove + -- Turn to final. self.Groove.name="Groove" self.Groove.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. self.Groove.Xmax= 0 -- Must be behind the boat. @@ -1145,7 +1144,7 @@ function AIRBOSS:_InitStennis() self.Groove.LimitZmin=nil self.Groove.LimitZmax=nil - -- Landing trap + -- In the Groove. self.Trap.name="Trap" self.Trap.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. self.Trap.Xmax= nil @@ -1511,66 +1510,6 @@ function AIRBOSS:_GetOnboardNumbers(group, playeronly) return numbers end ---- Create a new flight group. Usually when a flight appears in the CCA. --- @param #AIRBOSS self --- @param Wrapper.Group#GROUP group Aircraft group. --- @return #AIRBOSS.Flightitem Flight group. -function AIRBOSS:_CreateFlightGroup(group) - - -- Flight group name - local groupname=group:GetName() - local human=self:_IsHuman(group) - - -- Queue table item. - local flight={} --#AIRBOSS.Flightitem - flight.group=group - flight.groupname=group:GetName() - flight.nunits=#group:GetUnits() - flight.time=timer.getAbsTime() - flight.dist0=group:GetCoordinate():Get2DDistance(self:GetCoordinate()) - flight.flag=USERFLAG:New(groupname) - flight.flag:Set(-100) - flight.ai=not human - flight.actype=group:GetTypeName() - flight.onboardnumbers=self:_GetOnboardNumbers(group) - flight.section={} - flight.case=self.case - - if human then - - -- Attach player data to flight. - local playerData=self:_GetPlayerDataGroup(group) - flight.player=playerData - - -- Message to player. - MESSAGE:New(string.format("%s, your flight is registered within CCA.", playerData.name), 10, "MARSHAL"):ToClient(playerData.client) - - else - -- Nothing to do for AI. - end - - -- Add to known flights inside CCA zone. - table.insert(self.flights, flight) - - return flight -end - ---- Remove a flight group. --- @param #AIRBOSS self --- @param Wrapper.Group#GROUP group Aircraft group. --- @return #AIRBOSS.Flightitem Flight group. -function AIRBOSS:_RemoveFlightGroup(group) - local groupname=group:GetName() - for i,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.Flightitem - if flight.groupname==groupname then - self:I(string.format("Removing flight group %s (not in CCA).", groupname)) - table.remove(self.flights, i) - return - end - end -end - --- Orbit at a specified position at a specified alititude with a specified speed. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. @@ -1912,26 +1851,124 @@ function AIRBOSS:_CollapseMarshalStack(patternflight, refuel) end end ---- Remove a group from a queue. --- @param #AIRBOSS self --- @param #table queue The queue from which the group will be removed. --- @param Wrapper.Group#GROUP group Group that will be removed from queue. -function AIRBOSS:_RemoveGroupFromQueue(queue, group) +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FLIGHT & PLAYER functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - local name=group:GetName() +--- Create a new flight group. Usually when a flight appears in the CCA. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #AIRBOSS.Flightitem Flight group. +function AIRBOSS:_CreateFlightGroup(group) + + -- Flight group name + local groupname=group:GetName() + local human=self:_IsHuman(group) + + -- Queue table item. + local flight={} --#AIRBOSS.Flightitem + flight.group=group + flight.groupname=group:GetName() + flight.nunits=#group:GetUnits() + flight.time=timer.getAbsTime() + flight.dist0=group:GetCoordinate():Get2DDistance(self:GetCoordinate()) + flight.flag=USERFLAG:New(groupname) + flight.flag:Set(-100) + flight.ai=not human + flight.actype=group:GetTypeName() + flight.onboardnumbers=self:_GetOnboardNumbers(group) + flight.section={} + flight.case=self.case + + if human then - for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Flightitem + -- Attach player data to flight. + local playerData=self:_GetPlayerDataGroup(group) + flight.player=playerData - if flight.groupname==name then - self:I(self.lid..string.format("Removing group %s from queue.", name)) - table.remove(queue, i) - return - end + -- Message to player. + MESSAGE:New(string.format("%s, your flight is registered within CCA.", playerData.name), 10, "MARSHAL"):ToClient(playerData.client) + + else + -- Nothing to do for AI. end + -- Add to known flights inside CCA zone. + table.insert(self.flights, flight) + + return flight end + +--- Initialize player data after birth event of player unit. +-- @param #AIRBOSS self +-- @param #string unitname Name of the player unit. +-- @return #AIRBOSS.PlayerData Player data. +function AIRBOSS:_NewPlayer(unitname) + + -- Get player unit and name. + local playerunit, playername=self:_GetPlayerUnitAndName(unitname) + + if playerunit and playername then + + -- Player data. + local playerData={} --#AIRBOSS.PlayerData + + -- Player unit, client and callsign. + playerData.unit = playerunit + playerData.name = playername + playerData.group = playerunit:GetGroup() + playerData.callsign = playerData.unit:GetCallsign() + playerData.client = CLIENT:FindByName(unitname, nil, true) + playerData.actype = playerunit:GetTypeName() + playerData.onboard = self:_GetOnboardNumberPlayer(playerData.group) + playerData.seclead = playername + + -- Number of passes done by player. + playerData.passes=playerData.passes or 0 + + -- LSO grades. + playerData.grades=playerData.grades or {} + + -- Attitude monitor. + playerData.attitudemonitor=false + + -- Set difficulty level. + playerData.difficulty=playerData.difficulty or AIRBOSS.Difficulty.EASY + + -- Init stuff for this round. + playerData=self:_InitPlayer(playerData) + + -- Return player data table. + return playerData + end + + return nil +end + +--- Initialize player data by (re-)setting parmeters to initial values. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return #AIRBOSS.PlayerData Initialized player data. +function AIRBOSS:_InitPlayer(playerData) + self:I(self.lid..string.format("Initializing player data for %s callsign %s.", playerData.name, playerData.callsign)) + + playerData.step=AIRBOSS.PatternStep.UNDEFINED + playerData.groove={} + playerData.debrief={} + playerData.holding=nil + playerData.lig=false + playerData.patternwo=false + playerData.waveoff=false + playerData.bolter=false + playerData.boltered=false + playerData.landed=false + playerData.Tlso=timer.getTime() + + return playerData +end + + --- Get flight from group. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Group that will be removed from queue. @@ -1956,27 +1993,117 @@ function AIRBOSS:_GetFlightFromGroupInQueue(group, queue) return nil, nil end ---- Remove a group from a queue when all aircraft of that group have landed. +--- Check if a group is in a queue. +-- @param #AIRBOSS self +-- @param #table queue The queue to check. +-- @param Wrapper.Group#GROUP group +-- @return #boolean If true, group is in the queue. False otherwise. +function AIRBOSS:_InQueue(queue, group) + local name=group:GetName() + for _,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.Flightitem + if name==flight.groupname then + return true + end + end + return false +end + + +--- Remove a flight group. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #AIRBOSS.Flightitem Flight group. +function AIRBOSS:_RemoveFlightGroup(group) + local groupname=group:GetName() + for i,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.Flightitem + if flight.groupname==groupname then + self:I(string.format("Removing flight group %s (not in CCA).", groupname)) + table.remove(self.flights, i) + return + end + end +end + +--- Remove a flight group from a queue. +-- @param #AIRBOSS self +-- @param #table queue The queue from which the group will be removed. +-- @param #AIRBOSS.Flightitem flight Flight group that will be removed from queue. +function AIRBOSS:_RemoveFlightFromQueue(queue, flight) + + -- Loop over all flights in group. + for i,_flight in pairs(queue) do + local qflight=_flight --#AIRBOSS.Flightitem + + -- Check for name. + if qflight.groupname==flight.groupname then + self:I(self.lid..string.format("Removing flight group %s from queue.", flight.groupname)) + table.remove(queue, i) + return + end + end + +end + + +--- Remove a group from a queue. -- @param #AIRBOSS self -- @param #table queue The queue from which the group will be removed. -- @param Wrapper.Group#GROUP group Group that will be removed from queue. -function AIRBOSS:_RemoveQueue(queue, group) +function AIRBOSS:_RemoveGroupFromQueue(queue, group) + -- Group name. local name=group:GetName() - + + -- Loop over all flights in group. for i,_flight in pairs(queue) do local flight=_flight --#AIRBOSS.Flightitem + -- Check for name. if flight.groupname==name then + self:I(self.lid..string.format("Removing group %s from queue.", name)) + table.remove(queue, i) + return + end + end + +end + +--- Remove a unit from a flight group (e.g. when landed) and update all queues if the whole flight group is gone. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit to be removed. +function AIRBOSS:_RemoveUnitFromFlight(unit) + + -- Check if unit exists. + if unit then + + -- Get group. + local group=unit:GetGroup() - -- Decrease number of units in group. - flight.nunits=flight.nunits-1 - - if flight.nunits==0 then - self:I(self.lid..string.format("FF removing group %s from queue.", name)) - table.remove(queue, i) - end + -- Check if group exists. + if group then + + -- Get flight. + local flight=self:_GetFlightFromGroupInQueue(group, self.flights) + -- Check if flight exists. + if flight then + + -- TODO: Improve this and remove the explicit unit. Make unit array for flight! + -- Decrease number of units in group. + flight.nunits=flight.nunits-1 + + -- Check if no units are left. + if flight.nunits==0 then + + -- Remove flight from all queues. + self:_RemoveGroupFromQueue(self.flights, group) + self:_RemoveGroupFromQueue(self.Qmarshal, group) + self:_RemoveGroupFromQueue(self.Qpattern, group) + end + + end end end @@ -2159,13 +2286,12 @@ function AIRBOSS:OnEventBirth(EventData) self:T(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - -- Check if aircraft type the player occupies is carrier capable. local rightaircraft=self:_IsCarrierAircraft(_unit) if rightaircraft==false then local text=string.format("Player aircraft type %s not supported by AIRBOSS class.", _unit:GetTypeName()) - --MESSAGE:New(text, 30, "ERROR", true):ToGroup(_group) - self:E(self.lid..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:T(self.lid..text) return end @@ -2242,12 +2368,11 @@ function AIRBOSS:OnEventLand(EventData) -- We did land. playerData.landed=true - -- Unkonwn step. + -- Unkonwn step until we now more. playerData.step=AIRBOSS.PatternStep.UNDEFINED -- Call trapped function in 3 seconds to make sure we did not bolter. - SCHEDULER:New(nil, self._Trapped,{self, playerData, dist}, 3) - + SCHEDULER:New(nil, self._Trapped,{self, playerData, dist}, 3) end else @@ -2264,12 +2389,9 @@ function AIRBOSS:OnEventLand(EventData) local lp=coord:MarkToAll(text) coord:SmokeGreen() - - -- AI: Decrease number of units in flight and remove group from pattern queue if all units landed. - if self:_InQueue(self.Qpattern, EventData.IniGroup) then - self:_RemoveQueue(self.Qpattern, EventData.IniGroup) - end - + + -- AI always lands ==> remove unit from flight group and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) end end @@ -2287,89 +2409,20 @@ function AIRBOSS:OnEventCrash(EventData) self:I(self.lid.."CRASH: group = "..tostring(EventData.IniGroupName)) self:I(self.lid.."CARSH: player = "..tostring(_playername)) - -- TODO: Update queues! - -- TODO: Decrease number of units in group! if _unit and _playername then self:I(self.lid..string.format("Player %s crashed!",_playername)) else self:I(self.lid..string.format("AI unit %s crashed!", EventData.IniUnitName)) end + + -- Remove unit from flight and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) end - - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- PATTERN functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Initialize player data after birth event of player unit. --- @param #AIRBOSS self --- @param #string unitname Name of the player unit. --- @return #AIRBOSS.PlayerData Player data. -function AIRBOSS:_NewPlayer(unitname) - - -- Get player unit and name. - local playerunit, playername=self:_GetPlayerUnitAndName(unitname) - - if playerunit and playername then - - -- Player data. - local playerData={} --#AIRBOSS.PlayerData - - -- Player unit, client and callsign. - playerData.unit = playerunit - playerData.name = playername - playerData.group = playerunit:GetGroup() - playerData.callsign = playerData.unit:GetCallsign() - playerData.client = CLIENT:FindByName(unitname, nil, true) - playerData.actype = playerunit:GetTypeName() - playerData.onboard = self:_GetOnboardNumberPlayer(playerData.group) - playerData.seclead = playername - - -- Number of passes done by player. - playerData.passes=playerData.passes or 0 - - -- LSO grades. - playerData.grades=playerData.grades or {} - - -- Attitude monitor. - playerData.attitudemonitor=false - - -- Set difficulty level. - playerData.difficulty=playerData.difficulty or AIRBOSS.Difficulty.NORMAL - - -- Init stuff for this round. - playerData=self:_InitPlayer(playerData) - - -- Return player data table. - return playerData - end - - return nil -end - ---- Initialize player data by (re-)setting parmeters to initial values. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data. --- @return #AIRBOSS.PlayerData Initialized player data. -function AIRBOSS:_InitPlayer(playerData) - self:I(self.lid..string.format("New approach of player %s.", playerData.callsign)) - - playerData.step=AIRBOSS.PatternStep.UNDEFINED - - playerData.groove={} - playerData.debrief={} - playerData.patternwo=false - playerData.lig=false - playerData.waveoff=false - playerData.bolter=false - playerData.boltered=false - playerData.landed=false - playerData.holding=nil - playerData.Tlso=timer.getTime() - - return playerData -end --- Holding. -- @param #AIRBOSS self @@ -2571,7 +2624,7 @@ function AIRBOSS:_Descent4k(playerData) -- Abort condition check. if self:_CheckAbort(X, Z, self.Descent4k) then self:_AbortPattern(playerData, X, Z, self.Descent4k) - return + --return end -- Check if we are in front of the boat (diffX > 0). @@ -2609,7 +2662,7 @@ function AIRBOSS:_Platform(playerData) -- Abort condition check. if self:_CheckAbort(X, Z, self.Platform) then self:_AbortPattern(playerData, X, Z, self.Platform) - return + --return end -- Check if we are inside the moving zone. @@ -2650,7 +2703,7 @@ function AIRBOSS:_DirtyUp(playerData) -- Abort condition check. if self:_CheckAbort(X, Z, self.DirtyUp) then self:_AbortPattern(playerData, X, Z, self.DirtyUp) - return + --return end -- Check if we are inside the moving zone. @@ -2697,7 +2750,7 @@ function AIRBOSS:_Bullseye(playerData) -- Abort condition check. if self:_CheckAbort(X, Z, self.Bullseye) then self:_AbortPattern(playerData, X, Z, self.Bullseye) - return + --return end -- Check if we are inside the moving zone. @@ -2739,7 +2792,7 @@ function AIRBOSS:_Upwind(playerData) -- Abort condition check. if self:_CheckAbort(X, Z, self.Upwind) then - self:_AbortPattern(playerData, X, Z, self.Upwind) + self:_AbortPattern(playerData, X, Z, self.Upwind, true) return end @@ -2781,7 +2834,7 @@ function AIRBOSS:_Break(playerData, part) -- Check abort conditions. if self:_CheckAbort(X, Z, breakpoint) then - self:_AbortPattern(playerData, X, Z, breakpoint) + self:_AbortPattern(playerData, X, Z, breakpoint, true) return end @@ -2853,7 +2906,7 @@ function AIRBOSS:_Abeam(playerData) -- Check abort conditions. if self:_CheckAbort(X, Z, self.Abeam) then - self:_AbortPattern(playerData, X, Z, self.Abeam) + self:_AbortPattern(playerData, X, Z, self.Abeam, true) return end @@ -2897,7 +2950,7 @@ function AIRBOSS:_Ninety(playerData) -- Check abort conditions. if self:_CheckAbort(X, Z, self.Ninety) then - self:_AbortPattern(playerData, X, Z, self.Ninety) + self:_AbortPattern(playerData, X, Z, self.Ninety, true) return end @@ -2946,7 +2999,7 @@ function AIRBOSS:_Wake(playerData) -- Check abort conditions. if self:_CheckAbort(X, Z, self.Wake) then - self:_AbortPattern(playerData, X, Z, self.Wake) + self:_AbortPattern(playerData, X, Z, self.Wake, true) return end @@ -2987,7 +3040,7 @@ function AIRBOSS:_Final(playerData) -- In front of carrier or more than 4 km behind carrier. if self:_CheckAbort(X, Z, self.Groove) then - self:_AbortPattern(playerData, X, Z, self.Groove) + self:_AbortPattern(playerData, X, Z, self.Groove, true) return end @@ -3060,7 +3113,7 @@ function AIRBOSS:_Groove(playerData) -- Check abort conditions. if self:_CheckAbort(X, Z, self.Trap) then - self:_AbortPattern(playerData, X, Z, self.Trap) + self:_AbortPattern(playerData, X, Z, self.Trap, true) return end @@ -3261,21 +3314,21 @@ function AIRBOSS:_Trapped(playerData, X) -- Little offset for the exact wire positions. local wdx=0 - -- Which wire was caught? + -- Which wire was caught? X>0 since calculated as distance! local wire - if Xmath.abs(self.carrierparam.wire1+wdx) then wire=1 - elseif Xmath.abs(self.carrierparam.wire2+wdx) then wire=2 - elseif Xmath.abs(self.carrierparam.wire3+wdx) then wire=3 - elseif Xmath.abs(self.carrierparam.wire4+wdx) then wire=4 else wire=0 end - local text=string.format("TRAPPED! %d-wire.", wire) + local text=string.format("Trapped! %d-wire.", wire) self:_SendMessageToPlayer(text, 10, playerData) local text2=string.format("Distance X=%.1f meters resulted in a %d-wire estimate.", X, wire) @@ -3947,22 +4000,42 @@ end -- @param #AIRBOSS.Checkpoint posData Checkpoint data. function AIRBOSS:_TooFarOutText(X, Z, posData) - local text="You are too far " + -- Intro. + local text="you are too " + -- X text. local xtext=nil if posData.Xmin and XposData.Xmax then - xtext="behind" + if posData.LimitXmax>=0 then + xtext="far ahead of " + else + xtext="close to " + end end + -- Z text. local ztext=nil if posData.Zmin and ZposData.Zmax then - ztext="starboard (right) of" + if posData.Zmax>=0 then + ztext="far starboard of " + else + ztext="too close to " + end end + -- Combine X-Z text. if xtext and ztext then text=text..xtext.." and "..ztext elseif xtext then @@ -3971,7 +4044,13 @@ function AIRBOSS:_TooFarOutText(X, Z, posData) text=text..ztext end - text=text.." the carrier." + -- Complete the sentence + text=text.."the carrier." + + -- If no case could be identified. + if xtext==nil and ztext==nil then + text="you are too far from where you should be!" + end return text end @@ -3981,28 +4060,35 @@ end -- @param #AIRBOSS.PlayerData playerData Player data. -- @param #number X X distance player to carrier. -- @param #number Z Z distance player to carrier. +-- @param #boolean patternwo (Optional) Pattern wave off. -- @param #AIRBOSS.Checkpoint posData Checkpoint data. -function AIRBOSS:_AbortPattern(playerData, X, Z, posData) +function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) -- Text where we are wrong. - local toofartext=self:_TooFarOutText(X, Z, posData) - - -- Send message to player. - self:_SendMessageToPlayer(toofartext.." Depart and re-enter!", 15, playerData, false) + local text=self:_TooFarOutText(X, Z, posData) -- 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) + local dtext=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..dtext) --MESSAGE:New(text, 60):ToAllIf(self.Debug) - -- Add to debrief. - self:_AddToSummary(playerData, string.format("%s", playerData.step), string.format("Pattern wave off: %s", toofartext)) + if patternwo then - -- Pattern wave off! - playerData.patternwo=true + -- Pattern wave off! + playerData.patternwo=true + + -- Tell player to depart. + text=text.." Depart and re-enter!" + + -- Add to debrief. + self:_AddToSummary(playerData, string.format("%s", playerData.step), string.format("Pattern wave off: %s", text)) - -- Next step debrief. - playerData.step=AIRBOSS.PatternStep.DEBRIEF + -- Next step debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + end + + -- Message to player. + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 20) end @@ -4256,20 +4342,20 @@ function AIRBOSS:_Debrief(playerData) -- LSO grade message. local text=string.format("%s %.1f PT - %s", grade, points, analysis) - text=text..string.format("Your detailed debriefing can now be seen in F10 radio menu.") + text=text..string.format("Detailed debriefing can be found in the F10 radio menu.") self:MessageToPlayer(playerData,text, "LSO", "", 30, true) - -- New approach. + -- Check if boltered or waved off? if playerData.boltered or playerData.waveoff or playerData.patternwo then - -- TODO: can become nil when I crashed and changed to observer. + -- TODO: Can become nil when I crashed and changed to observer. Which events are captured? Nil check for unit? -- Get heading and distance to register zone ~3 NM astern. local heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) local distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) + -- Re-enter message. local text=string.format("fly heading %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) - self:_SendMessageToPlayer(text, 10, playerData, false, nil, 30) self:MessageToPlayer(playerData, text, "LSO", nil, 10) -- Next step? @@ -4278,7 +4364,9 @@ function AIRBOSS:_Debrief(playerData) -- TODO: CASE III: After bolter/wo turn left and climb to 1200 ft and re-enter pattern? -- TODO: CASE III: After pattern wo? No idea... - -- + -- Get flight. + + --[[ local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flight) if flight then @@ -4304,15 +4392,31 @@ function AIRBOSS:_Debrief(playerData) else end - playerData.step=AIRBOSS.PatternStep.COMMENCING + ]] + + + playerData.step=AIRBOSS.PatternStep.COMMENCING - elseif playerData.landed then + elseif playerData.landed and not playerData.unit:InAir() then + + -- Remove player unit from flight and all queues. + self:_RemoveUnitFromFlight(playerData.unit) + + -- Message to player. + self:MessageToPlayer(playerData, "Welcome to the carrier!", "LSO", nil, 10) + else + + -- Message to player. + self:MessageToPlayer(playerData, "Undefined state after landing! Please report.", "ERROR", nil, 10) + + -- Next step. + playerData.step=AIRBOSS.PatternStep.UNDEFINED end - -- Next step. - playerData.step=AIRBOSS.PatternStep.UNDEFINED + MESSAGE:New(string.format("Player step %s.", playerData.step)) + end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4474,21 +4578,6 @@ function AIRBOSS:_IsHuman(group) return false end ---- Check if a group is in the queue. --- @param #AIRBOSS self --- @param #table queue The queue to check. --- @param Wrapper.Group#GROUP group --- @return #boolean If true, group is in the queue. False otherwise. -function AIRBOSS:_InQueue(queue, group) - local name=group:GetName() - for _,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Flightitem - if name==flight.groupname then - return true - end - end - return false -end --- Get player data from unit object -- @param #AIRBOSS self From c5171e8722d160ecd41bdc29188935e68016317b Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 25 Nov 2018 00:05:31 +0100 Subject: [PATCH 057/147] RECOVERYTANKER v0.9.4 --- Moose Development/Moose/Core/Radio.lua | 9 +- Moose Development/Moose/Ops/Airboss.lua | 6 +- .../Moose/Ops/RecoveryTanker.lua | 453 +++++++++++++----- 3 files changed, 327 insertions(+), 141 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 1966dad47..ac45e3cc0 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -533,11 +533,6 @@ function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) return self end - if self.Positionable:IsAir() then - --TODO: set TACANMode="Y" - self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft! The BEACON is not emitting.", self.Positionable}) - end - -- Beacon type. local Type=BEACON.Type.TACAN @@ -548,6 +543,10 @@ function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) local AA=self.Positionable:IsAir() if AA then System=BEACON.System.TACAN_TANKER + -- Check if "Y" mode is selected for aircraft. + if Mode~="Y" then + self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable}) + end end -- Attached unit. diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index d21fa38e2..17668a747 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -3556,7 +3556,7 @@ function AIRBOSS:_GetRelativeHeading(unit, runway) rhdg=rhdg-self.carrierparam.rwyangle end - -- TODO another way would be to get the heading of the carrier and the heading of the unit and calc difference? + --[[ -- Heading of unit. local unitheading=unit:GetHeading() @@ -3570,8 +3570,8 @@ function AIRBOSS:_GetRelativeHeading(unit, runway) carrierheading=self:GetHeading(false) end - rhdg=unitheading-carrierheading - + local rhdg=unitheading-carrierheading + ]] -- Return heading in degrees. return rhdg diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 683a821b6..2b4149dca 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -20,6 +20,7 @@ --- RECOVERYTANKER class. -- @type RECOVERYTANKER -- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. -- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. -- @field #string carriertype Carrier type. -- @field #string tankergroupname Name of the late activated tanker template group. @@ -28,6 +29,8 @@ -- @field Core.Radio#BEACON beacon Tanker TACAN beacon. -- @field #number TACANchannel TACAN channel. Default 1. -- @field #string TACANmode TACAN mode, i.e. "X" or "Y". Default "Y". +-- @field #string TACANmorse TACAN morse code. Three letters identifying the TACAN station. Default "TKR". +-- @field #boolean TACANon If true, TACAN is automatically activated. If false, TACAN is disabled. -- @field #number speed Tanker speed when flying pattern. -- @field #number altitude Tanker orbit pattern altitude. -- @field #number distStern Race-track distance astern. @@ -39,6 +42,8 @@ -- @field #boolean respawn If true, tanker be respawned (default). If false, no respawning will happen. -- @field #boolean respawninair If true, tanker will always be respawned in air. This has no impact on the initial spawn setting. -- @field #boolean uncontrolledac If true, use and uncontrolled tanker group already present in the mission. +-- @field DCS#Vec3 orientation Orientation of the carrier. Used to monitor changes and update the pattern if heading changes significantly. +-- @field Core.Point#COORDINATE position Positon of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. -- @extends Core.Fsm#FSM --- Recovery Tanker. @@ -112,6 +117,7 @@ -- @field #RECOVERYTANKER RECOVERYTANKER = { ClassName = "RECOVERYTANKER", + Debug = true, carrier = nil, carriertype = nil, tankergroupname = nil, @@ -120,6 +126,8 @@ RECOVERYTANKER = { beacon = nil, TACANchannel = nil, TACANmode = nil, + TACANmorse = nil, + TACANon = nil, altitude = nil, speed = nil, distStern = nil, @@ -131,19 +139,24 @@ RECOVERYTANKER = { respawn = nil, respawninair = nil, uncontrolledac = nil, + orientation = nil, + position = nil, } - --- Class version. -- @field #string version -RECOVERYTANKER.version="0.9.3" +RECOVERYTANKER.version="0.9.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Smarter pattern update function. E.g. (small) zone around carrier. Only update position when carrier leaves zone or changes heading? +-- TODO: Is alive check for tanker. +-- TODO: Trace functions self:T instead of self:I for less output. +-- TODO: Make pattern update parameters (distance, orientation) input parameters. -- TODO: Write documenation. +-- DONE: Add FSM event for pattern update. +-- DONE: Smarter pattern update function. E.g. (small) zone around carrier. Only update position when carrier leaves zone or changes heading? -- DONE: Set AA TACAN. -- DONE: Add refueling event/state. -- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. @@ -174,6 +187,9 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- Tanker group name. self.tankergroupname=tankergroupname + -- Save self in static object. Easier to retrieve later. + self.carrier:SetState(self.carrier, "RECOVERYTANKER", self) + -- Init default parameters. self:SetPatternUpdateInterval() self:SetAltitude() @@ -194,12 +210,13 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- Add FSM transitions. -- From State --> Event --> To State - self:AddTransition("Stopped", "Start", "Running") - self:AddTransition("*", "Refuel", "Refueling") - self:AddTransition("*", "Run", "Running") - self:AddTransition("Running", "RTB", "Returning") - self:AddTransition("*", "Status", "*") - self:AddTransition("*", "Stop", "Stopped") + self:AddTransition("Stopped", "Start", "Running") -- Start the FSM. + self:AddTransition("*", "Refuel", "Refueling") -- Tanker starts to refuel. + self:AddTransition("*", "Run", "Running") -- Tanker starts normal operation again. + self:AddTransition("Running", "RTB", "Returning") -- Tanker is returning to base (for fuel). + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("Running", "PatternUpdate", "*") -- Update pattern wrt to carrier. + self:AddTransition("*", "Stop", "Stopped") -- Stop the FSM. --- Triggers the FSM event "Start" that starts the recovery tanker. Initializes parameters and starts event handlers. @@ -214,8 +231,8 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) --- Triggers the FSM event "Refuel" when the tanker is refueling another aircraft. -- @function [parent=#RECOVERYTANKER] Refuel - -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. -- @param #RECOVERYTANKER self + -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. --- Triggers delayed the FSM event "Refuel" when the tanker is refueling another aircraft. -- @function [parent=#RECOVERYTANKER] __Refuel @@ -237,11 +254,33 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) --- Triggers the FSM event "RTB" that sends the tanker home. -- @function [parent=#RECOVERYTANKER] RTB -- @param #RECOVERYTANKER self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. --- Triggers the FSM event "RTB" that sends the tanker home after a delay. -- @function [parent=#RECOVERYTANKER] __RTB -- @param #RECOVERYTANKER self -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + + + --- Triggers the FSM event "Status" that updates the tanker status. + -- @function [parent=#RECOVERYTANKER] Status + -- @param #RECOVERYTANKER self + + --- Triggers the delayed FSM event "Status" that updates the tanker status. + -- @function [parent=#RECOVERYTANKER] __Status + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "PatternUpdate" that updates the pattern of the tanker wrt to the carrier position. + -- @function [parent=#RECOVERYTANKER] PatternUpdate + -- @param #RECOVERYTANKER self + + --- Triggers the delayed FSM event "PatternUpdate" that updates the pattern of the tanker wrt to the carrier position. + -- @function [parent=#RECOVERYTANKER] __PatternUpdate + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. --- Triggers the FSM event "Stop" that stops the recovery tanker. Event handlers are stopped. @@ -400,31 +439,50 @@ function RECOVERYTANKER:SetUseUncontrolledAircraft() return self end + +--- Disable automatic TACAN activation. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTACANoff() + self.TACANon=false + return self +end + --- Set TACAN channel of tanker. -- @param #RECOVERYTANKER self -- @param #number channel TACAN channel. Default 1. -- @param #string mode TACAN mode, i.e. "X" or "Y". Default "Y". +-- @param #string morse TACAN morse code identifier. Three letters. Default "TKR". -- @return #RECOVERYTANKER self -function RECOVERYTANKER:SetTACAN(channel, mode) +function RECOVERYTANKER:SetTACAN(channel, mode, morse) self.TACANchannel=channel or 1 self.TACANmode=mode or "Y" + self.TACANmorse=morse or "TKR" + self.TACANon=true return self end ---- Check if tanker is returning to base. +--- Check if tanker is currently returning to base. -- @param #RECOVERYTANKER self -- @return #boolean If true, tanker is returning to base. function RECOVERYTANKER:IsReturning() return self:is("Returning") end ---- Check if tanker is operating. +--- Check if tanker is currently operating. -- @param #RECOVERYTANKER self -- @return #boolean If true, tanker is operating. function RECOVERYTANKER:IsRunning() return self:is("Running") end +--- Check if tanker is currently refueling another aircraft. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is refueling. +function RECOVERYTANKER:IsRefueling() + return self:is("Refueling") +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM states ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -441,9 +499,9 @@ function RECOVERYTANKER:onafterStart(From, Event, To) -- Handle events. self:HandleEvent(EVENTS.EngineShutdown) - self:HandleEvent(EVENTS.Refueling) - self:HandleEvent(EVENTS.RefuelingStop) - self:HandleEvent(EVENTS.Crash) + self:HandleEvent(EVENTS.Refueling, self._RefuelingStart) --Need explcit functions sice OnEventRefueling and OnEventRefuelingStop did not hook. + self:HandleEvent(EVENTS.RefuelingStop, self._RefuelingStop) + --self:HandleEvent(EVENTS.Crash) -- Spawn tanker. local Spawn=SPAWN:New(self.tankergroupname):InitUnControlled(false) @@ -467,7 +525,7 @@ function RECOVERYTANKER:onafterStart(From, Event, To) self.tanker=Spawn:SpawnFromCoordinate(Carrier) -- Initial route. - self:_InitRoute(15, 1, 2) + self:_InitRoute(15, 1) else -- Check if an uncontrolled tanker group was requested. @@ -495,18 +553,24 @@ function RECOVERYTANKER:onafterStart(From, Event, To) end -- Initialize route. - self:_InitRoute(30, 10, 1) + self:_InitRoute(15, 1) end -- Create tanker beacon. - self.beacon=BEACON:New(self.tanker:GetUnit(1)) - self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, "TKR", true) + if self.TACANon then + self:_ActivateTACAN(2) + end + -- Get initial orientation and position of carrier. + self.orientation=self.carrier:GetOrientationX() + self.position=self.carrier:GetCoordinate() + -- Init status check. self:__Status(10) end + --- On after Status event. Checks player status. -- @param #RECOVERYTANKER self -- @param #string From From state. @@ -522,16 +586,42 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) local text=string.format("Recovery tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) self:I(text) - - -- Check if tanker is running and not RTBing. + -- Check if tanker is running and not RTBing or refueling. if self:IsRunning() then -- Check fuel. if fuelself.dTupdate then - self:_PatternUpdate() + if updatepattern then + self:PatternUpdate() end end @@ -553,41 +646,59 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) self:__Status(-60) end ---- On before RTB event. Check if takeoff type is air and if so respawn the tanker and deny RTB transition. +--- On after "PatternUpdate" event. Updates the racetrack pattern of the tanker wrt the carrier position. -- @param #RECOVERYTANKER self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @return #boolean If true, transition is allowed. -function RECOVERYTANKER:onbeforeRTB(From, Event, To) +function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) - -- Check if spawn in air is activated. - if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then + -- Debug message. + self:I(string.format("Updating recovery tanker %s orbit.", self.tanker:GetName())) + + -- Carrier heading. + local hdg=self.carrier:GetHeading() - -- Check that respawn should happen. - if self.respawn then + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() - -- Debug message. - local text=string.format("Respawning tanker %s.", self.tanker:GetName()) - self:I(text) - - -- Respawn tanker. - self.tanker:InitHeading(self.tanker:GetHeading()) - self.tanker=self.tanker:Respawn(nil, true) - - -- Create tanker beacon. - self.beacon=BEACON:New(self.tanker:GetUnit(1)) - self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, "TKR", true) - - -- Update Pattern in 2 seconds. Need to give a bit time so that the respawned group is in the game. - SCHEDULER:New(nil, self._PatternUpdate, {self}, 2) - - -- Deny transition to RTB. - return false - end + -- Define race-track pattern. + local p0=self.tanker:GetCoordinate():Translate(2000, self.tanker:GetHeading()) + local p1=Carrier:SetAltitude(self.altitude):Translate(self.distStern, hdg) + local p2=Carrier:SetAltitude(self.altitude):Translate(self.distBow, hdg) + + -- Set orbit task. + local taskorbit=self.tanker:TaskOrbit(p1, self.altitude, self.speed, p2) + + -- Debug markers. + if self.Debug then + p0:MarkToAll("Waypoint P0 " ..self.tanker:GetName()) + p1:MarkToAll("Racetrack P1 "..self.tanker:GetName()) + p2:MarkToAll("Racetrack P2 "..self.tanker:GetName()) + self.tanker:SmokeRed() end + + -- Waypoints array. + local wp={} + + -- New waypoint with orbit pattern task. + wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil , self.speed, {}, "Current Position") + wp[2]=p0:WaypointAirTurningPoint(nil, self.speed, {taskorbit}, "Tanker Orbit") - return true + -- Initialize WP and route tanker. + self.tanker:WayPointInitialize(wp) + + -- Task combo. + local tasktanker = self.tanker:EnRouteTaskTanker() + local taskroute = self.tanker:TaskRoute(wp) + -- Note that tasktanker has to come first. Otherwise it does not work! + local taskcombo = self.tanker:TaskCombo({tasktanker, taskroute}) + + -- Set task. + self.tanker:SetTask(taskcombo, 1) + + -- Set update time. + self.Tupdate=timer.getTime() end --- On after "RTB" event. Send tanker back to carrier. @@ -595,24 +706,28 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function RECOVERYTANKER:onafterRTB(From, Event, To) +-- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. +function RECOVERYTANKER:onafterRTB(From, Event, To, airbase) - -- Debug message. - local text=string.format("Tanker %s returning to airbase %s.", self.tanker:GetName(), self.airbase:GetName()) - self:I(text) - - -- Waypoint array. - local wp={} - - -- Set landing waypoint. - wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil, 300, {}, "Current Position") - wp[2]=self.carrier:GetCoordinate():WaypointAirLanding(300, self.airbase, nil, "Landing on Carrier") + -- Default is the home base. + airbase=airbase or self.airbase - -- Initialize WP and route tanker. - self.tanker:WayPointInitialize(wp) + -- Debug message. + local text=string.format("Tanker %s returning to airbase %s.", self.tanker:GetName(), airbase:GetName()) + self:I(text) - -- Set task. - self.tanker:Route(wp, 1) + -- Waypoint array. + local wp={} + + -- Set landing waypoint. + wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil, 300, {}, "Current Position") + wp[2]=airbase:GetCoordinate():WaypointAirLanding(300, airbase, nil, "Land at airbase") + + -- Initialize WP and route tanker. + self.tanker:WayPointInitialize(wp) + + -- Set task. + self.tanker:Route(wp, 1) end --- On after Stop event. Unhandle events and stop status updates. @@ -622,6 +737,8 @@ end -- @param #string To To state. function RECOVERYTANKER:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.EngineShutdown) + self:UnHandleEvent(EVENTS.Refueling) + self:UnHandleEvent(EVENTS.RefuelingStop) --self:UnHandleEvent(EVENTS.Land) end @@ -651,12 +768,13 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) -- Respawn tanker. self.tanker=group:RespawnAtCurrentAirbase() - -- Create tanker beacon. - self.beacon=BEACON:New(self.tanker:GetUnit(1)) - self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, "TKR", true) + -- Create tanker beacon and activate TACAN. + if self.TACANon then + self:_ActivateTACAN(2) + end -- Initial route. - self:_InitRoute() + self:_InitRoute(15, 1) end end @@ -665,7 +783,7 @@ end --- Event handler for refueling started. -- @param #RECOVERYTANKER self -- @param Core.Event#EVENTDATA EventData Event data. -function RECOVERYTANKER:OnEventRefuel(EventData) +function RECOVERYTANKER:_RefuelingStart(EventData) if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() then @@ -682,6 +800,9 @@ function RECOVERYTANKER:OnEventRefuel(EventData) -- Info message. self:I(string.format("Recovery tanker %s started refueling unit %s", self.tanker:GetName(), unit:GetName())) + + -- FMS state "Refueling". + self:Refuel(unit) end @@ -690,7 +811,7 @@ end --- Event handler for refueling stopped. -- @param #RECOVERYTANKER self -- @param Core.Event#EVENTDATA EventData Event data. -function RECOVERYTANKER:OnEventRefuelStop(EventData) +function RECOVERYTANKER:_RefuelingStop(EventData) if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() then @@ -707,26 +828,46 @@ function RECOVERYTANKER:OnEventRefuelStop(EventData) -- Info message. self:I(string.format("Recovery tanker %s stopped refueling unit %s", self.tanker:GetName(), unit:GetName())) - + + -- FSM state "Running". + self:Run() end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- ROUTE functions +-- MISC functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Task function to +-- @param #RECOVERYTANKER self +function RECOVERYTANKER:_InitPatternTaskFunction() + + -- Name of the warehouse (static) object. + local carriername=self.carrier:GetName() + + -- Task script. + local DCSScript = {} + DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername) -- The carrier unit that holds the self object. + DCSScript[#DCSScript+1] = string.format('local mytanker = mycarrier:GetState(mycarrier, \"RECOVERYTANKER\") ') -- Get the RECOVERYTANKER self object. + DCSScript[#DCSScript+1] = string.format('mytanker:PatternUpdate()') -- Call the function, e.g. mytanker.(self) + + -- Create task. + local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + + return DCSTask +end + + --- Init waypoint after spawn. -- @param #RECOVERYTANKER self --- @param #number dist Distance [NM] of initial waypoint astern carrier. Default 30 NM. --- @param #number Tstart Time in minutes before the tanker starts its pattern. Default 10 min. +-- @param #number dist Distance [NM] of initial waypoint astern carrier. Default 15 NM. -- @param #number delay Delay before routing in seconds. Default 1 second. -function RECOVERYTANKER:_InitRoute(dist, Tstart, delay) +function RECOVERYTANKER:_InitRoute(dist, delay) -- Defaults. - dist=UTILS.NMToMeters(dist or 30) - Tstart=(Tstart or 10)*60 + dist=UTILS.NMToMeters(dist or 15) delay=delay or 1 -- Debug message. @@ -738,75 +879,121 @@ function RECOVERYTANKER:_InitRoute(dist, Tstart, delay) -- Carrier heading. local hdg=self.carrier:GetHeading() - -- First waypoint is 50 km behind the boat. + -- First waypoint is ~15 NM behind the boat. local p=Carrier:Translate(-dist, hdg):SetAltitude(self.altitude) - -- Debug mark - p:MarkToAll(string.format("Init WP: alt=%d ft, speed=%d kts", UTILS.MetersToFeet(self.altitude), UTILS.MpsToKnots(self.speed))) + -- Debug mark. + if self.Debug then + p:MarkToAll(string.format("Init WP: alt=%d ft, speed=%d kts", UTILS.MetersToFeet(self.altitude), UTILS.MpsToKnots(self.speed))) + end + + -- Task to update pattern when wp 2 is reached. + local task=self:_InitPatternTaskFunction() -- Waypoints. local wp={} - wp[1]=Carrier:WaypointAirTakeOffParking() - wp[2]=p:WaypointAirTurningPoint(nil, self.speed, nil, "Stern") - + if self.takeoff==SPAWN.Takeoff.Air then + wp[#wp+1]=self.tanker:GetCoordinate():SetAltitude(self.altitude):WaypointAirTurningPoint(nil, self.speed, {}, "Spawn Position") + else + wp[#wp+1]=Carrier:WaypointAirTakeOffParking() + end + wp[#wp+1]=p:WaypointAirTurningPoint(nil, self.speed, {task}, "Begin Pattern") + -- Set route. self.tanker:Route(wp, delay) - -- No update yet. + -- No update yet, wait until the function is called (avoids checks if pattern update is needed). self.Tupdate=nil - - -- Update pattern in ~10 minutes. - SCHEDULER:New(nil, self._PatternUpdate, {self}, Tstart) end - ---- Function to update the race-track pattern of the tanker wrt to the carrier position. +--- Check if heading or position have changed significantly. -- @param #RECOVERYTANKER self -function RECOVERYTANKER:_PatternUpdate() +-- @param #number dt Time since last update in seconds. +-- @return #boolean If true, heading and/or position have changed more than 10 degrees or 10 km, respectively. +function RECOVERYTANKER:_CheckPatternUpdate(dt) - -- Debug message. - self:I(string.format("Updating recovery tanker %s orbit.", self.tanker:GetName())) + -- Assume no update necessary. + local update=false + + -- Get current position and orientation of carrier. + local pos=self.carrier:GetCoordinate() + local vC=self.carrier:GetOrientationX() + + -- Check if tanker is running and last updated is more than 10 minutes ago. + if self:IsRunning() and dt>10*60 then + + -- Last saved orientation of carrier. + local vP=self.orientation - -- Carrier heading. - local hdg=self.carrier:GetHeading() + -- We only need the X-Z plane. + vC.y=0 ; vP.y=0 + + -- Get angle between the two orientation vectors in rad. + local rhdg=math.deg(math.acos(UTILS.VecDot(vC,vP)/UTILS.VecNorm(vC)/UTILS.VecNorm(vP))) - -- Carrier position. - local Carrier=self.carrier:GetCoordinate() + -- Check if orientation changed. + -- TODO: make 5 deg input variable. + if math.abs(rhdg)>5 then + self:I(string.format("Carrier heading changed by %d degrees. Updating recovery tanker pattern.", rhdg)) + update=true + end + + -- Get distance to saved position. + local dist=pos:Get2DDistance(self.position) + + -- Check if carrier moved more than 10 km. + -- TODO: make 10 km input variable. + if dist/1000>10 then + self:I(string.format("Carrier position changed by %.1f km. Updating recovery tanker pattern.", dist/1000)) + update=true + end + + end - -- Define race-track pattern. - local p0=self.tanker:GetCoordinate():Translate(1000, self.tanker:GetHeading()) - local p1=Carrier:SetAltitude(self.altitude):Translate(self.distStern, hdg) - local p2=Carrier:SetAltitude(self.altitude):Translate(self.distBow, hdg) - - -- Set orbit task. - local taskorbit=self.tanker:TaskOrbit(p1, self.altitude, self.speed, p2) - - -- Debug markers. - if self.Debug then - p0:MarkToAll("Waypoint P0 " ..self.tanker:GetName()) - p1:MarkToAll("Racetrack P1 "..self.tanker:GetName()) - p2:MarkToAll("Racetrack P2 "..self.tanker:GetName()) + -- If pattern is updated then update orientation AND positon. + -- But only if last update is less then 10 minutes ago. + if update then + self.orientation=vC + self.position=pos end - -- Waypoints array. - local wp={} - - -- New waypoint with orbit pattern task. - --wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil , self.speed, {}, "Current Position") - wp[1]=p0:WaypointAirTurningPoint(nil, self.speed, {taskorbit}, "Tanker Orbit") - - -- Initialize WP and route tanker. - self.tanker:WayPointInitialize(wp) - - -- Task combo. - local tasktanker = self.tanker:EnRouteTaskTanker() - local taskroute = self.tanker:TaskRoute(wp) - -- Note that tasktanker has to come first. Otherwise it does not work! - local taskcombo = self.tanker:TaskCombo({tasktanker, taskroute}) - - -- Set task. - self.tanker:SetTask(taskcombo, 1) - - -- Set update time. - self.Tupdate=timer.getTime() + return update end + +--- Activate TACAN of tanker. +-- @param #RECOVERYTANKER self +-- @param #number delay Delay in seconds. +function RECOVERYTANKER:_ActivateTACAN(delay) + + if delay and delay>0 then + + -- Schedule TACAN activation. + SCHEDULER:New(nil,self._ActivateTACAN, {self}, delay) + + else + + -- Get tanker unit. + local unit=self.tanker:GetUnit(1) + + -- Check if unit is alive. + if unit:IsAlive() then + + -- Debug message. + self:I(string.format("Activating recovery tanker TACAN beacon: channel=%d mode=%s, morse=%s.", self.TACANchannel, self.TACANmode, self.TACANmorse)) + + -- Create a new beacon and activate TACAN. + self.beacon=BEACON:New(unit) + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) + + else + self:E("ERROR: Recovery tanker is not alive!") + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + From 8cea5017480d8237e6dd479616c32b40b354f7ab Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 25 Nov 2018 11:10:11 +0100 Subject: [PATCH 058/147] RESCUEHELO v0.9.4 --- Moose Development/Moose/Ops/RescueHelo.lua | 98 ++++++++++++++++++++-- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 93f2ce8b1..691cf583b 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -21,6 +21,7 @@ --- RESCUEHELO class. -- @type RESCUEHELO -- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode on/off. -- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. -- @field #string carriertype Carrier type. -- @field #string helogroupname Name of the late activated helo template group. @@ -37,6 +38,12 @@ -- @field #boolean respawn If true, helo be respawned (default). If false, no respawning will happen. -- @field #boolean respawninair If true, helo will always be respawned in air. This has no impact on the initial spawn setting. -- @field #boolean uncontrolledac If true, use and uncontrolled helo group already present in the mission. +-- @field #boolean rescueon If true, helo will rescue crashed pilots. If false, no recuing will happen. +-- @field #number rescueduration Time the rescue helicopter hovers over the crash site in seconds. +-- @field #number rescuespeed Speed in m/s the rescue helicopter hovers at over the crash site. +-- @field #boolean rescuestopboat If true, stop carrier during rescue operations. +-- @field #boolean carrierstop If true, route of carrier was stopped. +-- @field #number HeloFuel0 Initial fuel of helo in percent. Necessary due to DCS bug that helo with full tank does not return fuel via API function. -- @extends Core.Fsm#FSM --- Rescue Helo @@ -128,6 +135,7 @@ -- @field #RESCUEHELO RESCUEHELO = { ClassName = "RESCUEHELO", + Debug = false, carrier = nil, carriertype = nil, helogroupname = nil, @@ -144,20 +152,26 @@ RESCUEHELO = { respawn = nil, respawninair = nil, uncontrolledac = nil, + rescueon = nil, + rescueduration = nil, + rescuespeed = nil, + rescuestopboat = nil, + HeloFuel0 = nil, + carrierstop = false, } --- Class version. -- @field #string version -RESCUEHELO.version="0.9.3" +RESCUEHELO.version="0.9.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Add option to stop carrier while rescue operation is in progress. --- TODO: Possibility to add already present/spawned aircraft, e.g. for warehouse. --- TODO: Add option to deactivate the rescueing. +-- TODO: Add option to stop carrier while rescue operation is in progress? -- TODO: Write documenation. +-- DONE: Add option to deactivate the rescueing. +-- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. -- DONE: Add rescue event when aircraft crashes. -- DONE: Make offset input parameter. @@ -195,7 +209,11 @@ function RESCUEHELO:New(carrierunit, helogroupname) self:SetAltitude() self:SetOffsetX() self:SetOffsetZ() + self:SetRescueOn() self:SetRescueZone() + self:SetRescueHoverSpeed() + self:SetRescueDuration() + self:SetRescueStopBoatOff() ----------------------- --- FSM Transitions --- @@ -295,6 +313,57 @@ function RESCUEHELO:SetRescueZone(radius) return self end +--- Set rescue hover speed. +-- @param #RESCUEHELO self +-- @param #number speed Speed in km/h. Default 25 km/h. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueHoverSpeed(speed) + self.rescuespeed=UTILS.KmphToMps(speed or 25) + return self +end + +--- Set rescue duration. This is the time it takes to rescue a pilot at the crash site. +-- @param #RESCUEHELO self +-- @param #number duration Duration in minutes. Default 5 min. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueDuration(duration) + self.rescueduration=(duration or 5)*60 + return self +end + +--- Activate rescue option. Crashed and ejected pilots will be rescued. This is the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueOn() + self.rescueon=true + return self +end + +--- Deactivate rescue option. Crashed and ejected pilots will not be rescued. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueOff() + self.rescueon=false + return self +end + +--- Stop carrier during rescue operations. NOT WORKING! +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueStopBoatOn() + self.rescuestopboat=true + return self +end + +--- Do not stop carrier during rescue operations. This is the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueStopBoatOff() + self.rescuestopboat=false + return self +end + + --- Set takeoff type. -- @param #RESCUEHELO self -- @param #number takeofftype Takeoff type. Default SPAWN.Takeoff.Hot. @@ -493,7 +562,7 @@ function RESCUEHELO:_OnEventCrashOrEject(EventData) coord:MarkToCoalition(string.format("Crash site of unit %s.", unitname), self.helo:GetCoalition()) -- Only rescue if helo is "running" and not, e.g., rescuing already. - if self:IsRunning() then + if self:IsRunning() and self.rescueon then self:Rescue(coord) end @@ -652,9 +721,17 @@ function RESCUEHELO:onafterRun(From, Event, To) -- Restart formation if stopped. if self.formation:Is("Stopped") then + self:I(string.format("Restarting formation of rescue helo %s.", self.helo:GetName())) self.formation:Start() end + -- Restart route of carrier if it was stopped. + if self.carrierstop then + self:I("Carrier resuming route after rescue operation.") + self.carrier:RouteResume() + self.carrierstop=false + end + end --- On after "Rescue" event. Helo will fly to the given coordinate, orbit there for 5 minutes and then return to the carrier. @@ -678,8 +755,8 @@ function RESCUEHELO:onafterRescue(From, Event, To, RescueCoord) local RescueTask={} RescueTask.id="ControlledTask" RescueTask.params={} - RescueTask.params.task=self.helo:TaskOrbit(RescueCoord, 20, 2) - RescueTask.params.stopCondition={duration=300} + RescueTask.params.task=self.helo:TaskOrbit(RescueCoord, 20, self.rescuespeed) + RescueTask.params.stopCondition={duration=self.rescueduration} -- Set Waypoints. wp[1]=self.helo:GetCoordinate():WaypointAirTurningPoint(nil, 200, {}, "Current Position") @@ -694,6 +771,13 @@ function RESCUEHELO:onafterRescue(From, Event, To, RescueCoord) -- Stop formation. self.formation:Stop() + + -- Stop carrier. + if self.rescuestopboat then + self:I("Stopping carrier for rescue operation.") + self.carrier:RouteStop() + self.carrierstop=true + end end From 58886dc42dc0b4c8195f42b9945ffa11bb9ccd3a Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 26 Nov 2018 00:43:38 +0100 Subject: [PATCH 059/147] AIRBOSS v0.3.5 --- Moose Development/Moose/Core/Zone.lua | 43 +- Moose Development/Moose/Ops/Airboss.lua | 807 ++++++++++++++++-------- 2 files changed, 570 insertions(+), 280 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 244d5de66..c1370b2a1 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -1380,16 +1380,15 @@ end --- Smokes the zone boundaries in a color. -- @param #ZONE_POLYGON_BASE self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. -- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) +function ZONE_POLYGON_BASE:SmokeZone( SmokeColor, Segments ) self:F2( SmokeColor ) - local i - local j - local Segments = 10 + Segments=Segments or 10 - i = 1 - j = #self._.Polygon + local i=1 + local j=#self._.Polygon while i <= #self._.Polygon do self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) @@ -1410,6 +1409,38 @@ function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) end +--- Flare the zone boundaries in a color. +-- @param #ZONE_POLYGON_BASE self +-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments ) + self:F2(FlareColor) + + Segments=Segments or 10 + + local i=1 + local j=#self._.Polygon + + while i <= #self._.Polygon do + self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) + + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x + local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y + + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. + local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) + POINT_VEC2:New( PointX, PointY ):Flare(FlareColor) + end + j = i + i = i + 1 + end + + return self +end + + --- Returns if a location is within the zone. diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 17668a747..58db6086d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1,4 +1,4 @@ ---- **Functional** - (R2.5) - Manages aircraft operations on carriers. +--- **Ops** - (R2.5) - Manages aircraft operations on carriers. -- -- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers. -- @@ -146,7 +146,7 @@ AIRBOSS = { tanker = nil, warehouse = nil, recoverytime = {}, - holdoffset = 0, + holdingoffset= 0, } --- Player aircraft types capable of landing on carriers. @@ -407,32 +407,6 @@ AIRBOSS.GroovePos={ -- @field #number Speed Optimal speed at this point. -- @field #table Checklist Table of checklist text items to display at this point. ---- Player data table holding all important parameters of each player. --- @type AIRBOSS.PlayerData --- @field Wrapper.Unit#UNIT unit Aircraft of the player. --- @field #string name Player name. --- @field Wrapper.Client#CLIENT client Client object of player. --- @field Wrapper.Group#GROUP group Aircraft group the player is in. --- @field #string callsign Callsign of player. --- @field #string actype Aircraft type. --- @field #string onboard Onboard number. --- @field #string difficulty Difficulty level. --- @field #string step Coming pattern step. --- @field #number passes Number of passes. --- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. --- @field #table debrief Debrief analysis of the current step of this pass. --- @field #table grades LSO grades of player passes. --- @field #boolean holding If true, player is in holding zone. --- @field #boolean landed If true, player landed or attempted to land. --- @field #boolean bolter If true, LSO told player to bolter. --- @field #boolean boltered If true, player boltered. --- @field #boolean waveoff If true, player was waved off during final approach. --- @field #boolean patternwo If true, player was waved of during the pattern. --- @field #boolean lig If true, player was long in the groove. --- @field #number Tlso Last time the LSO gave an advice. --- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. --- @field #string seclead Name of section lead. --- @field #table menu F10 radio menu --- Parameters of a flight group. -- @type AIRBOSS.Flightitem @@ -442,20 +416,46 @@ AIRBOSS.GroovePos={ -- @field #number dist0 Distance to carrier in meters when the group was first detected inside the CCA. -- @field #number time Time the flight was added to the queue. -- @field Core.UserFlag#USERFLAG flag User flag for triggering events for the flight. --- @field #boolean ai If true, flight is AI. If false, flight is a human player. --- @field #AIRBOSS.PlayerData player Player data for human pilots. +-- @field #boolean ai If true, flight is AI. +-- @field #boolean player If true, flight is a human player. -- @field #string actype Aircraft type name. -- @field #table onboardnumbers Onboard numbers of aircraft in the group. -- @field #number case Recovery case of flight. -- @field #table section Other human flight groups belonging to this flight. This flight is the lead. +--- Player data table holding all important parameters of each player. +-- @type AIRBOSS.PlayerData +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string name Player name. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field #string callsign Callsign of player. +-- @field #string onboard Onboard number. +-- @field #string difficulty Difficulty level. +-- @field #string step Coming pattern step. +-- @field #number passes Number of passes. +-- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. +-- @field #table debrief Debrief analysis of the current step of this pass. +-- @field #table grades LSO grades of player passes. +-- @field #boolean holding If true, player is in holding zone. +-- @field #boolean landed If true, player landed or attempted to land. +-- @field #boolean boltered If true, player boltered. +-- @field #boolean waveoff If true, player was waved off during final approach. +-- @field #boolean patternwo If true, player was waved of during the pattern. +-- @field #boolean lig If true, player was long in the groove. +-- @field #number Tlso Last time the LSO gave an advice. +-- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. +-- @field #string seclead Name of section lead. +-- @field #table menu F10 radio menu +-- @extends #AIRBOSS.Flightitem + + --- Main radio menu. -- @field #table MenuF10 AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.4" +AIRBOSS.version="0.3.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -561,9 +561,19 @@ function AIRBOSS:New(carriername, alias) self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, 0.5*1000, {dx=-UTILS.NMToMeters(3), dy=100, relative_to_unit=true}) -- CASE II/III moving zones. - self.zonePlatform = ZONE_UNIT:New("Platform Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters(20), theta=-171, relative_to_unit=true}) - self.zoneDirtyup = ZONE_UNIT:New("Dirty Up Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters(10), theta=-171, relative_to_unit=true}) - self.zoneBullseye = ZONE_UNIT:New("Bulleye Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters( 3), theta=-171, relative_to_unit=true}) + local angle=180+self.carrierparam.rwyangle + self.zonePlatform = ZONE_UNIT:New("Platform Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters(20), theta=angle, relative_to_unit=true}) + self.zoneDirtyup = ZONE_UNIT:New("Dirty Up Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters(10), theta=angle, relative_to_unit=true}) + self.zoneBullseye = ZONE_UNIT:New("Bulleye Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters( 3), theta=angle, relative_to_unit=true}) + + -- Smoke zones. + if self.Debug then + --self.zoneInitial:SmokeZone(SMOKECOLOR.White, 90) + --self.zonePlatform:SmokeZone(SMOKECOLOR.Orange, 90) + --self.zoneDirtyup:SmokeZone(SMOKECOLOR.Blue, 90) + --self.zoneBullseye:SmokeZone(SMOKECOLOR.Red, 90) + --local zp=self:_GetCase23ValidZone():SmokeZone(SMOKECOLOR.Green, 45) + end -- CCA 50 NM radius zone around the carrier. self:SetCarrierControlledArea() @@ -1200,7 +1210,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) alt=UTILS.FeetToMeters(1200) - dist=-UTILS.NMToMeters(3) + dist=-UTILS.NMToMeters(3) elseif step==AIRBOSS.PatternStep.INITIAL then @@ -1300,7 +1310,7 @@ function AIRBOSS:_CheckQueue() local _,npattern=self:_GetQueueInfo(self.Qpattern) -- Get number of flight groups(!) in marshal pattern. - local nmarshal=#self.Qmarshal + local nmarshal,_=self:_GetQueueInfo(self.Qmarshal) -- Check if there are flights in marshal strack and if the pattern is free. if nmarshal>0 and npattern<1 then @@ -1423,7 +1433,7 @@ function AIRBOSS:_ScanCarrierZone() -- Create a new flight group if knownflight then - self:I(string.format("Known CCA flight group %s of type %s", groupname, actype)) + self:I(string.format("Known flight group %s of type %s in CCA.", groupname, actype)) if knownflight.ai then -- Get distance to carrier. @@ -1435,7 +1445,7 @@ function AIRBOSS:_ScanCarrierZone() end end else - self:I(string.format("UNKNOWN CCA flight group %s of type %s", groupname, actype)) + self:I(string.format("UNKNOWN flight group %s of type %s detected inside CCA.", groupname, actype)) self:_CreateFlightGroup(group) end @@ -1513,11 +1523,10 @@ end --- Orbit at a specified position at a specified alititude with a specified speed. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. --- @param #AIRBOSS.Flightitem flight Flight group. -function AIRBOSS:_MarshalPlayer(playerData, flight) +function AIRBOSS:_MarshalPlayer(playerData) -- Check if flight is known to the airboss already. - if playerData and flight then + if playerData then -- Number of flight groups in stack. local ngroups,nunits=self:_GetQueueInfo(self.Qmarshal, self.case) @@ -1526,15 +1535,15 @@ function AIRBOSS:_MarshalPlayer(playerData, flight) local mystack=ngroups+1 -- Add group to marshal stack. - self:_AddMarshallGroup(flight, mystack) + self:_AddMarshallGroup(playerData, mystack) -- Set step to holding. playerData.step=AIRBOSS.PatternStep.HOLDING -- Set same stack for all flights in section. - for _,_flight in pairs(flight.section) do - local flight=_flight --#AIRBOSS.Flightitem - flight.player.step=AIRBOSS.PatternStep.HOLDING + for _,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + flight.step=AIRBOSS.PatternStep.HOLDING flight.flag:Set(mystack) end @@ -1731,6 +1740,7 @@ function AIRBOSS:_AddMarshallGroup(flight, flagvalue) -- Pressure. local P=UTILS.hPa2inHg(self:GetCoordinate():GetPressure()) + -- Get unit name for board number. local unitname=flight.group:GetUnit(1):GetName() -- TODO: Get correct board number if possible? @@ -1772,43 +1782,45 @@ end --- Collapse marshal stack. -- @param #AIRBOSS self --- @param #AIRBOSS.Flightitem patternflight Flight to go to pattern. --- @param #boolean refuel If true, patternflight wants to refuel and not go into pattern. -function AIRBOSS:_CollapseMarshalStack(patternflight, refuel) - self:I({flight=patternflight, refuel=refuel}) +-- @param #AIRBOSS.Flightitem flight Flight that left the marshal stack. +-- @param #boolean nopattern If true, flight does not go to pattern. +function AIRBOSS:_CollapseMarshalStack(flight, nopattern) + self:I({flight=flight, nopattern=nopattern}) -- Recovery case of flight. - local case=patternflight.case - local pstack=patternflight.flag:Get() + local case=flight.case + + -- Stack of flight. + local stack=flight.flag:Get() -- Decrease flag values of all flight groups in marshal stack. for _,_flight in pairs(self.Qmarshal) do - local flight=_flight --#AIRBOSS.Flightitem + local mflight=_flight --#AIRBOSS.Flightitem -- Only collaps stack of which the flight left. CASE II/III stack is the same. - if (case==1 and flight.case==1) or (case>1 and flight.case>1) then + if (case==1 and mflight.case==1) or (case>1 and mflight.case>1) then -- Get current flag/stack value. - local mstack=flight.flag:Get() + local mstack=mflight.flag:Get() -- Only collapse stacks above the new pattern flight. -- TODO: this will go wrong, if patternflight is not in marshal stack because it will have value -100 and all mstacks will be larger! -- Maybe need to set the initial value to 1000? Or check pstack>0? - if pstack>0 and mstack>pstack then + if stack>0 and mstack>stack then -- Decrease stack/flag by one ==> AI will go lower. - flight.flag:Set(mstack-1) + mflight.flag:Set(mstack-1) -- Inform players. - if flight.player then + if mflight.player then local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(mstack-1,case)) local text=string.format("descent to next lower stack at %d ft", alt) - self:MessageToPlayer(flight.player, text, "MARSHAL", nil, 10) + self:MessageToPlayer(mflight, text, "MARSHAL", nil, 10) end -- Also decrease flag for section members of flight. - for _,_sec in pairs(flight.section) do - local sec=_sec --#AIRBOSS.Flightitem + for _,_sec in pairs(mflight.section) do + local sec=_sec --#AIRBOSS.PlayerData sec.flag:Set(mstack-1) end @@ -1818,35 +1830,33 @@ function AIRBOSS:_CollapseMarshalStack(patternflight, refuel) end - if refuel then + if nopattern then -- Debug - self:I(self.lid..string.format("Flight %s is going for gas.", patternflight.groupname)) + self:I(self.lid..string.format("Flight %s is leaving stack but not going to pattern.", flight.groupname)) -- New time stamp for time in pattern. - patternflight.time=timer.getAbsTime() + flight.time=timer.getAbsTime() -- Set flag to -1. - patternflight.flag:Set(-1) - - -- TODO: Add to refueling queue. + flight.flag:Set(-1) else -- Debug - self:I(self.lid..string.format("Flight %s is going into pattern.", patternflight.groupname)) + self:I(self.lid..string.format("Flight %s is commencing pattern.", flight.groupname)) -- New time stamp for time in pattern. - patternflight.time=timer.getAbsTime() + flight.time=timer.getAbsTime() -- Decrease flag. - patternflight.flag:Set(pstack-1) + flight.flag:Set(stack-1) -- Add flight to pattern queue. - table.insert(self.Qpattern, patternflight) + table.insert(self.Qpattern, flight) -- Remove flight from marshal queue. - self:_RemoveGroupFromQueue(self.Qmarshal, patternflight.group) + self:_RemoveGroupFromQueue(self.Qmarshal, flight.group) end end @@ -1861,40 +1871,41 @@ end -- @return #AIRBOSS.Flightitem Flight group. function AIRBOSS:_CreateFlightGroup(group) - -- Flight group name - local groupname=group:GetName() - local human=self:_IsHuman(group) + -- Debug info. + self:I(self.lid..string.format("Creating new flight for group %s.", group:GetName())) - -- Queue table item. + -- New flight. local flight={} --#AIRBOSS.Flightitem - flight.group=group - flight.groupname=group:GetName() - flight.nunits=#group:GetUnits() - flight.time=timer.getAbsTime() - flight.dist0=group:GetCoordinate():Get2DDistance(self:GetCoordinate()) - flight.flag=USERFLAG:New(groupname) - flight.flag:Set(-100) - flight.ai=not human - flight.actype=group:GetTypeName() - flight.onboardnumbers=self:_GetOnboardNumbers(group) - flight.section={} - flight.case=self.case - if human then + if not self:_InQueue(self.flights,group) then + + -- Flight group name + local groupname=group:GetName() + local human=self:_IsHuman(group) - -- Attach player data to flight. - local playerData=self:_GetPlayerDataGroup(group) - flight.player=playerData + -- Queue table item. + flight.group=group + flight.groupname=group:GetName() + flight.nunits=#group:GetUnits() + flight.time=timer.getAbsTime() + flight.dist0=group:GetCoordinate():Get2DDistance(self:GetCoordinate()) + flight.flag=USERFLAG:New(groupname) + flight.flag:Set(-100) + flight.ai=not human + flight.actype=group:GetTypeName() + flight.onboardnumbers=self:_GetOnboardNumbers(group) + flight.section={} - -- Message to player. - MESSAGE:New(string.format("%s, your flight is registered within CCA.", playerData.name), 10, "MARSHAL"):ToClient(playerData.client) + -- TODO set elsewhere. + flight.case=self.case + + -- Add to known flights inside CCA zone. + table.insert(self.flights, flight) else - -- Nothing to do for AI. + self:E(self.lid..string.format("ERROR: Flight group %s already exists in self.flights!", group:GetName())) + return nil end - - -- Add to known flights inside CCA zone. - table.insert(self.flights, flight) return flight end @@ -1910,9 +1921,14 @@ function AIRBOSS:_NewPlayer(unitname) local playerunit, playername=self:_GetPlayerUnitAndName(unitname) if playerunit and playername then + + local group=playerunit:GetGroup() -- Player data. - local playerData={} --#AIRBOSS.PlayerData + local playerData --#AIRBOSS.PlayerData + + -- Create a flight group for the player. + playerData=self:_CreateFlightGroup(group) -- Player unit, client and callsign. playerData.unit = playerunit @@ -1920,7 +1936,6 @@ function AIRBOSS:_NewPlayer(unitname) playerData.group = playerunit:GetGroup() playerData.callsign = playerData.unit:GetCallsign() playerData.client = CLIENT:FindByName(unitname, nil, true) - playerData.actype = playerunit:GetTypeName() playerData.onboard = self:_GetOnboardNumberPlayer(playerData.group) playerData.seclead = playername @@ -1960,7 +1975,6 @@ function AIRBOSS:_InitPlayer(playerData) playerData.lig=false playerData.patternwo=false playerData.waveoff=false - playerData.bolter=false playerData.boltered=false playerData.landed=false playerData.Tlso=timer.getTime() @@ -2091,16 +2105,14 @@ function AIRBOSS:_RemoveUnitFromFlight(unit) if flight then -- TODO: Improve this and remove the explicit unit. Make unit array for flight! + -- Decrease number of units in group. flight.nunits=flight.nunits-1 -- Check if no units are left. if flight.nunits==0 then - -- Remove flight from all queues. - self:_RemoveGroupFromQueue(self.flights, group) - self:_RemoveGroupFromQueue(self.Qmarshal, group) - self:_RemoveGroupFromQueue(self.Qpattern, group) + self:_RemoveFlight(flight) end end @@ -2109,6 +2121,25 @@ function AIRBOSS:_RemoveUnitFromFlight(unit) end +--- Remove a flight from all queues. Also set player step to undefined if applicable. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Flightitem flight The flight to be removed. +function AIRBOSS:_RemoveFlight(flight) + + -- Remove flight from all queues. + self:_RemoveFlightFromQueue(self.Qmarshal, flight) + self:_RemoveFlightFromQueue(self.Qpattern, flight) + + -- Set Playerstep to undefined. + if flight.player then + flight.step=AIRBOSS.PatternStep.UNDEFINED + else + -- Remove flight completely + self:_RemoveFlightFromQueue(self.flights, flight) + end + +end + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Player Status ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2306,7 +2337,7 @@ function AIRBOSS:OnEventBirth(EventData) --self:RadioTransmission(self.LSOradio, self.radiocall.LONGINGROOVE, false, 20) -- Start in the groove for debugging. - self.groovedebug=false + self.groovedebug=true end end @@ -2317,13 +2348,18 @@ end function AIRBOSS:OnEventLand(EventData) self:F3({eventland = EventData}) + -- Get unit name that landed. local _unitName=EventData.IniUnitName + + -- Check if this was a player. local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + -- Debug output. 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)) + -- Check if player or AI landed. if _unit and _playername then -- Human Player landed. @@ -2353,6 +2389,9 @@ function AIRBOSS:OnEventLand(EventData) local lp=coord:MarkToAll("Landing coord.") coord:SmokeGreen() + -- Get distances relative to + local X,Z,rho,phi=self:_GetDistances(_unit) + -- Landing distance to carrier position. local dist=coord:Get2DDistance(self:GetCoordinate()) @@ -2365,6 +2404,17 @@ function AIRBOSS:OnEventLand(EventData) local w3=self:GetCoordinate():Translate(self.carrierparam.wire3, hdg):MarkToAll("Wire 3") local w4=self:GetCoordinate():Translate(self.carrierparam.wire4, hdg):MarkToAll("Wire 4") + -- Get wire + local wire=self:_GetWire(dist) + + -- Aircraft type. + local _type=EventData.IniUnit:GetTypeName() + + -- Debug text. + local text=string.format("Player %s of type %s landed at dist=%.1f m. Trapped wire=%d.", EventData.IniUnitName, _type, dist, wire) + text=text..string.format("X=%.1f m, Z=%.1f m, rho=%.1f m, phi=%.1f deg.", X, Z, rho, phi) + self:I(self.lid..text) + -- We did land. playerData.landed=true @@ -2384,11 +2434,15 @@ function AIRBOSS:OnEventLand(EventData) -- Debug mark of player landing coord. local dist=coord:Get2DDistance(self:GetCoordinate()) - local text=string.format("AI landing dist=%.1f m", dist) - env.info(text) - - local lp=coord:MarkToAll(text) - coord:SmokeGreen() + -- Get wire + local wire=self:_GetWire(dist) + + -- Aircraft type. + local _type=EventData.IniUnit:GetTypeName() + + -- Debug text. + local text=string.format("AI %s of type %s landed at dist=%.1f m. Trapped wire=%d.", EventData.IniUnitName, _type, dist, wire) + self:I(self.lid..text) -- AI always lands ==> remove unit from flight group and queues. self:_RemoveUnitFromFlight(EventData.IniUnit) @@ -2431,10 +2485,9 @@ function AIRBOSS:_Holding(playerData) -- Player unit and flight. local unit=playerData.unit - local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) -- Current stack. - local stack=flight.flag:Get() + local stack=playerData.flag:Get() -- Pattern alitude. local patternalt, c1, c2=self:_GetMarshalAltitude(stack) @@ -2512,6 +2565,46 @@ function AIRBOSS:_Holding(playerData) self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 5) end + +--- Get CASE II/III box zone. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE_POLYGON_BASE Box zone. +function AIRBOSS:_GetCase23ValidZone() + + -- Radial. + local hdg=self:GetRadialCase3(false) + + self:I("FF radial case 3 = "..hdg) + + -- Width of the box. + local w=UTILS.NMToMeters(5) + -- Length of the box. + local l=UTILS.NMToMeters(50) + + -- Coordinate of the carrier. + local c0=self:GetCoordinate() + + -- Do not jump diagonal because it screws up the smoke zones. + local c1=c0:Translate(w, hdg+90) -- Starboard close + local c2=c1:Translate(l, hdg) -- Starboard far + local c4=c0:Translate(w, hdg-90) -- Port close + local c3=c4:Translate(l, hdg) -- Port far + + + -- Create an array of a square! + local p={} + p[1]=c1:GetVec2() + p[2]=c2:GetVec2() + p[3]=c3:GetVec2() + p[4]=c4:GetVec2() + + -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. + -- So stay 0-5 NM (+1 NM error margin) port of carrier. + local zone=ZONE_POLYGON_BASE:New("CASE II/III Valid Zone", p) + + return zone +end + --- Get holding zone of player. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. @@ -2520,17 +2613,16 @@ function AIRBOSS:_GetHoldingZone(playerData) -- Player unit and flight. local unit=playerData.unit - local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) -- Create a holding zone depending on recovery case. local zoneHolding=nil --Core.Zone#ZONE - if flight then + if unit:IsAlive() then -- Current stack. - local stack=flight.flag:Get() + local stack=playerData.flag:Get() - -- Stack is <= 0 ==> no marshal zone. + -- Stack is <= 0 ==> no marshal zone. if stack<=0 then return nil end @@ -2538,7 +2630,7 @@ function AIRBOSS:_GetHoldingZone(playerData) -- Pattern alitude. local patternalt, c1, c2=self:_GetMarshalAltitude(stack) - if self.case==1 then + if playerData.case==1 then -- CASE I -- Zone 2.5 NM port of carrier with a radius of 3 NM (holding pattern should be < 5 NM). @@ -2548,7 +2640,8 @@ function AIRBOSS:_GetHoldingZone(playerData) -- CASE II/II -- TODO: Include 15 or 30 degrees offset. - local hdg=self.carrier:GetHeading() + local hdg=self.carrier:GetHeading()-self.holdingoffset + local hdg=self:GetRadialCase3(false) -- Create an array of a square! local p={} @@ -2586,7 +2679,7 @@ function AIRBOSS:_Commencing(playerData) playerData.step=AIRBOSS.PatternStep.INITIAL else -- CASE III: Player has to start the descent at 4000 ft/min. - playerData.step=AIRBOSS.PatternStep.DESCENT4K + playerData.step=AIRBOSS.PatternStep.PLATFORM end end @@ -2674,18 +2767,21 @@ function AIRBOSS:_Platform(playerData) MESSAGE:New("Platform step reached", 5):ToAllIf(self.Debug) -- Get optimal altitiude. - local altitude=self:_GetAircraftParameters(playerData) + local altitude, aoa, distance, speed =self:_GetAircraftParameters(playerData) --TODO: check speed. - -- Get altitude. - local hint, debrief=self:_AltitudeCheck(playerData, altitude) + -- Get altitude hint. + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, altitude) + + -- Get altitude hint. + local hintSpeed, debriefSpeed=self:_AltitudeCheck(playerData, altitude) -- Message to player self:_SendMessageToPlayer(hint, 10, playerData) -- Debrief. - self:_AddToSummary(playerData, "Platform 5k", debrief) + --self:_AddToSummary(playerData, "Platform 5k", debrief) -- Next step: Dirty up and level out at 1200 ft. playerData.step=AIRBOSS.PatternStep.DIRTYUP @@ -3170,7 +3266,7 @@ function AIRBOSS:_Groove(playerData) elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then -- Debug. - self:_SendMessageToPlayer("IM", 8, playerData) + self:_SendMessageToPlayer("IM", 5, playerData) self:I(self.lid..string.format("FF IM=%d", rho)) -- Store data. @@ -3185,7 +3281,7 @@ function AIRBOSS:_Groove(playerData) if playerData.waveoff==false then -- Debug - self:_SendMessageToPlayer("IC", 8, playerData) + self:_SendMessageToPlayer("IC", 5, playerData) self:I(self.lid..string.format("FF IC=%d", rho)) -- Store data. @@ -3230,7 +3326,7 @@ function AIRBOSS:_Groove(playerData) local deltaT=time-playerData.Tlso -- Check if we are beween 3/4 NM and end of ship. - if rho>=RAR and rho=3 then + if rho>=RAR and rho=3 and playerData.waveoff==false then -- LSO call if necessary. self:_LSOadvice(playerData, glideslopeError, lineupError) @@ -3295,7 +3391,30 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA) return waveoff end +--- Get wire from landing position. +-- @param #AIRBOSS self +-- @param #number d Distance in meters wrt carrier position where player landed. +function AIRBOSS:_GetWire(d) + -- Little offset for the exact wire positions. + local wdx=0 + + -- Which wire was caught? X>0 since calculated as distance! + local wire + if d>math.abs(self.carrierparam.wire1+wdx) then + wire=1 + elseif d>math.abs(self.carrierparam.wire2+wdx) then + wire=2 + elseif d>math.abs(self.carrierparam.wire3+wdx) then + wire=3 + elseif d>math.abs(self.carrierparam.wire4+wdx) then + wire=4 + else + wire=99 + end + + return wire +end --- Trapped? -- @param #AIRBOSS self @@ -3311,23 +3430,10 @@ function AIRBOSS:_Trapped(playerData, X) if playerData.unit:InAir()==false then -- Seems we have successfully landed. - -- Little offset for the exact wire positions. - local wdx=0 - - -- Which wire was caught? X>0 since calculated as distance! - local wire - if X>math.abs(self.carrierparam.wire1+wdx) then - wire=1 - elseif X>math.abs(self.carrierparam.wire2+wdx) then - wire=2 - elseif X>math.abs(self.carrierparam.wire3+wdx) then - wire=3 - elseif X>math.abs(self.carrierparam.wire4+wdx) then - wire=4 - else - wire=0 - end + -- Get wire. + local wire=self:_GetWire(X) + -- Info to player. local text=string.format("Trapped! %d-wire.", wire) self:_SendMessageToPlayer(text, 10, playerData) @@ -3448,14 +3554,14 @@ function AIRBOSS:_Lineup(playerData, runway) local c={x=b.x-a.x, y=0, z=b.z-a.z} -- Current line up and error wrt to final heading of the runway. - local lineup=math.det(math.atan2(c.z, c.x)) + local lineup=math.deg(math.atan2(c.z, c.x)) -- Include runway. if runway then lineup=lineup-self.carrierparam.rwyangle end - return math.deg(lineup), UTILS.VecNorm(c) + return lineup, UTILS.VecNorm(c) end --- Get true (or magnetic) heading of carrier. @@ -3512,9 +3618,29 @@ function AIRBOSS:GetFinalBearing(magnetic) return fb end +--- Get radial, i.e. the final bearing FB-180 degrees including holding offset for Case II/III recoveries. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, magnetic radial is returned. Default is true radial. +-- @return #number Radial in degrees. +function AIRBOSS:GetRadialCase3(magnetic) + + -- Get radial. + local radial=self:GetFinalBearing(magnetic)-180 + + -- Holding offset angle (+-15 or 30 degrees usually) + radial=radial-self.holdingoffset + + -- Adjust for negative values. + if radial<0 then + radial=radial+360 + end + + return radial +end + --- Get radial, i.e. the final bearing FB-180 degrees. -- @param #AIRBOSS self --- @param #boolean magnetic If true, magnetic FB is returned. +-- @param #boolean magnetic If true, magnetic radial is returned. Default is true radial. -- @return #number Radial in degrees. function AIRBOSS:GetRadial(magnetic) @@ -4699,6 +4825,9 @@ function AIRBOSS:_AddF10Commands(_unitName) -- F10/Airboss//My Settings/Skil Level local _skillPath=missionCommands.addSubMenuForGroup(_gid, "Skill Level", _rootPath) + -- F10/Airboss//My Settings/Skil Level + local _helpPath=missionCommands.addSubMenuForGroup(_gid, "Help", _rootPath) + -- F10/Airboss//My Settings/Kneeboard local _kneeboardPath=missionCommands.addSubMenuForGroup(_gid, "Kneeboard", _rootPath) @@ -4712,14 +4841,19 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) missionCommands.addCommandForGroup(_gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) - + + -- F10/Airboss//Help + missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _helpPath, self._AttitudeMonitor, self, playername) + missionCommands.addCommandForGroup(_gid, "Smoke Marshal Zone", _helpPath, self._MarkMarshalZone, self, _unitName, false) + missionCommands.addCommandForGroup(_gid, "Flare Marshal Zone", _helpPath, self._MarkMarshalZone, self, _unitName, true) + missionCommands.addCommandForGroup(_gid, "Smoke CASE II/III Zones", _helpPath, self._MarkCase23Zones, self, _unitName, false) + missionCommands.addCommandForGroup(_gid, "Flare CASE II/III Zones", _helpPath, self._MarkCase23Zones, self, _unitName, true) + missionCommands.addCommandForGroup(_gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) + -- F10/Airboss//Kneeboard - missionCommands.addCommandForGroup(_gid, "Carrier Info", _kneeboardPath, self._DisplayCarrierInfo, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Weather Report", _kneeboardPath, self._DisplayCarrierWeather, self, _unitName) - missionCommands.addCommandForGroup(_gid, "My Status", _kneeboardPath, self._DisplayPlayerStatus, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _kneeboardPath, self._AttitudeMonitor, self, playername) - missionCommands.addCommandForGroup(_gid, "Smoke Marshal Zone", _kneeboardPath, self._SmokeMarshalZone, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Flare Marshal Zone", _kneeboardPath, self._FlareMarshalZone, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Carrier Info", _kneeboardPath, self._DisplayCarrierInfo, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Weather Report", _kneeboardPath, self._DisplayCarrierWeather, self, _unitName) + missionCommands.addCommandForGroup(_gid, "My Status", _kneeboardPath, self._DisplayPlayerStatus, self, _unitName) -- F10/Airboss// missionCommands.addCommandForGroup(_gid, "Request Marshal?", _rootPath, self._RequestMarshal, self, _unitName) @@ -4741,6 +4875,34 @@ end -- ROOT MENU ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Reset player status. Player is removed from all queues and its status is set to undefined. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_ResetPlayerStatus(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + local text="Status reset executed! You have been removed from all queues." + + if self:_InQueue(self.Qmarshal, playerData.group) then + self:_CollapseMarshalStack(playerData, true) + end + + -- Remove flight from queues. + self:_RemoveFlight(playerData) + + end + end +end + --- Request marshal. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. @@ -4755,16 +4917,43 @@ function AIRBOSS:_RequestMarshal(_unitName) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then + + -- Check if player is in CCA + local inCCA=playerData.unit:IsInZone(self.zoneCCA) - -- Get flight group. - local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) + if inCCA then - if flight then - self:_MarshalPlayer(playerData, flight) + if self:_InQueue(self.Qmarshal, playerData.group) then + + -- Flight group is already in marhal queue. + local text=string.format("%s, you are already in the Marshal queue. New marshal request denied!", playerData.name) + MESSAGE:New(text, 10, "MARSHAL"):ToClient(playerData.client) + + elseif self:_InQueue(self.Qpattern, playerData.group) then + + -- Flight group is already in pattern queue. + local text=string.format("%s, you are already in the Pattern queue. Marshal request denied!", playerData.name) + MESSAGE:New(text, 10, "MARSHAL"):ToClient(playerData.client) + + elseif not _unit:InAir() then + + -- Flight group is already in pattern queue. + local text=string.format("%s, you are not airborn. Marshal request denied!", playerData.name) + MESSAGE:New(text, 10, "MARSHAL"):ToClient(playerData.client) + + else + + -- Add flight to marshal stack. + self:_MarshalPlayer(playerData) + + end + else - -- Flight group does not exist yet. - local text=string.format("%s, you are not registered inside CCA yet. Marshal request denied!", playerData.name) + + -- Flight group is not in CCA yet. + local text=string.format("%s, you are not inside CCA yet. Marshal request denied!", playerData.name) MESSAGE:New(text, 10, "MARSHAL"):ToClient(playerData.client) + end end end @@ -4784,41 +4973,61 @@ function AIRBOSS:_RequestCommence(_unitName) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then - - -- Get flight group. - local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) - local text - if flight then + -- Check if unit is in CCA. + local text + if _unit:IsInZone(self.zoneCCA) then - -- Get stack value. - local stack=flight.flag:Get() + if self:_InQueue(self.Qmarshal, playerData.group) then - if stack>1 then - -- We are in a higher stack. - text="Negative ghostrider, it's not your turn yet!" - else + -- Flight group is already in marhal queue. + text=string.format("%s, you are already in the Marshal queue. Commence request denied!", playerData.name) - -- Number of aircraft currently in pattern. - local _,npattern=self:_GetQueueInfo(self.Qpattern) - - -- TODO: set nmax for pattern. Should be ~6 but let's make this 4. - if npattern>0 then - -- Patern is full! - text=string.format("Negative ghostrider, pattern is full! There are %d aircraft currently in pattern.", npattern) + elseif self:_InQueue(self.Qpattern, playerData.group) then + + -- Flight group is already in pattern queue. + text=string.format("%s, you are already in the Pattern queue. Commence request denied!", playerData.name) + + elseif not _unit:InAir() then + + -- Flight group is already in pattern queue. + text=string.format("%s, you are not airborn. Commence request denied!", playerData.name) + + else + + -- Get stack value. + local stack=playerData.flag:Get() + + if stack>1 then + -- We are in a higher stack. + text="Negative ghostrider, it's not your turn yet!" else - -- Positive response. - text="You are cleared for pattern. Proceed to initial." - - -- Set player step. - playerData.step=AIRBOSS.PatternStep.COMMENCING - - -- Collaps marshal stack. - self:_CollapseMarshalStack(flight) + + -- Number of aircraft currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + -- TODO: set nmax for pattern. Should be ~6 but let's make this 4. + if npattern>0 then + -- Patern is full! + text=string.format("Negative ghostrider, pattern is full! There are %d aircraft currently in pattern.", npattern) + else + -- Positive response. + if playerData.case==1 then + text="You are cleared for pattern. Proceed to initial." + else + text="You are cleared for pattern. Descent at 4k ft/min to platform at 5000 ft." + end + + -- Set player step. + playerData.step=AIRBOSS.PatternStep.COMMENCING + + -- Collaps marshal stack. + self:_CollapseMarshalStack(playerData, false) + end + end - + end - else -- This flight is not yet registered! text="Negative ghostrider, you are not yet registered inside the CCA yet!" @@ -4847,33 +5056,48 @@ function AIRBOSS:_RequestRefueling(_unitName) if playerData then + -- Check if there is a recovery tanker defined. local text if self.tanker then - - --TODO: request refueling if recovery tanker set! make refuelling queue. add refuelling step. - text="Player requested refueling. (not implemented yet)" - - -- Flight group. - local myflight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) - - if myflight then - - if self.tanker:IsRunning() then - text="Proceed to tanker at angels 6." - -- TODO: collaple stack. check in which queues flight is. + + -- Check if player is in CCA. + if _unit:IsInZone(self.zoneCCA) then + + -- Check if tanker is running or refueling or returning. + if self.tanker:IsRunning() or self.tanker:IsRefueling() then + + -- Get alt of tanker in angels. + local angels=UTILS.Round(UTILS.MetersToFeet(self.tanker.altitude)/1000, 0) + + -- Tanker is up and running. + text=string.format("Proceed to tanker at angels %d.", angels) + + --TODO: State TACAN channel of tanker if defined. + + -- Tanker is currently refueling. Inform player. + if self.tanker:IsRefueling() then + text=text.."\n Tanker is currently refueling. You might have to queue up." + end + + -- Collapse marshal stack if player is in queue. + if self:_InQueue(self.Qmarshal, playerData.group) then + -- TODO: What if only the player and not his section wants to refuel?! + self:_CollapseMarshalStack(playerData, true) + end elseif self.tanker:IsReturning() then - text="Tanker is currently returning to carrier. Request denied!" + -- Tanker is RTB. + text="Tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." end else - text="You are not registered in CCA zone yet." + text="You are not registered inside the CCA yet. Request denied!" end else - text="No refueling tanker available!" + text="No refueling tanker available. Request denied!" end -- Send message. - MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) + self:MessageToPlayer(playerData, text, "AIRBOSS") end end end @@ -4895,48 +5119,44 @@ function AIRBOSS:_SetSection(_unitName) -- Coordinate of flight lead. local mycoord=_unit:GetCoordinate() - -- Flight group. - local myflight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) - -- TODO: Only allow set section, if player is not in marshal stack yet. local text - if myflight then - -- Loop over all registered flights. - for _,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.Flightitem - - -- Only human flight groups excluding myself. - if flight.ai==false and flight.player and flight.groupname~=myflight.groupname then - - -- Distance to other group. - local distance=flight.group:GetCoordinate():Get2DDistance(mycoord) - - if distance<200 then - table.insert(myflight.section, flight) - end - - end - end + -- Loop over all registered flights. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.Flightitem - -- Info on section members. - if #myflight.section>0 then - text=string.format("Registered flight section") - text=text..string.format("- %s (lead)", myflight.player.name) - for _,_flight in paris(myflight.section) do - local flight=_flight --#AIRBOSS.Flightitem - text=text..string.format("- %s", flight.player.name) + -- Only human flight groups excluding myself. + if flight.ai==false and flight.groupname~=playerData.groupname then + + -- Distance to other group. + local distance=flight.group:GetCoordinate():Get2DDistance(mycoord) + + if distance<200 then + table.insert(playerData.section, flight) end - else - text="No other human flights found within radius of 200 meter radius!" + end - - else - text="You are not registered in CCA zone yet." end - - MESSAGE:New(text, 10, "MARSHALL"):ToClient(playerData.callsign) + + -- Info on section members. + if #playerData.section>0 then + text=string.format("Registered flight section:") + text=text..string.format("- %s (lead)", playerData.name) + for _,_flight in paris(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + text=text..string.format("- %s", flight.name) + flight.seclead=playerData.name + -- Inform player that he is now part of a section. + self:MessageToPlayer(flight, string.format("Your section lead is not %s", playerData.name), self.carrier:GetName(), "", 10) + end + else + text="No other human flights found within radius of 200 meter radius!" + end + + -- Message to section lead. + self:MessageToPlayer(playerData, text, self.alias, "", 10) end end @@ -5156,6 +5376,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) -- Message text. local text=string.format("%s info:\n", self.alias) + text=text..string.format("=============================================\n") text=text..string.format("Carrier state %s\n", self:GetState()) text=text..string.format("Case %d Recovery\n", self.case) text=text..string.format("BRC %03d°\n", self:GetBRC()) @@ -5224,7 +5445,7 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) -- Report text. text=text..string.format("Weather Report at Carrier %s:\n", self.alias) - text=text..string.format("--------------------------------------------------\n") + 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) @@ -5258,54 +5479,60 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) -- Player data. local text=string.format("Status of player %s (%s)\n", playerData.name, playerData.callsign) - text=text..string.format("======================================================\n") + text=text..string.format("=============================================\n") text=text..string.format("Current step: %s\n", playerData.step) text=text..string.format("Skil level: %s\n", playerData.difficulty) text=text..string.format("Aircraft: %s\n", playerData.actype) text=text..string.format("Board number: %s\n", playerData.onboard) text=text..string.format("Fuel: %.1f %%\n", playerData.unit:GetFuel()*100) + local stack=playerData.flag:Get() + local stackalt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) + text=text..string.format("Flag/stack: %d\n", stack) + text=text..string.format("Stack alt: %d ft\n", stackalt) text=text..string.format("Group: %s\n", playerData.group:GetName()) - text=text..string.format("# units: %s\n", #playerData.group:GetUnits()) - text=text..string.format("Section Lead: %s\n", tostring(playerData.seclead)) - - -- Flight data (if available). - local flight=self:_GetFlightFromGroupInQueue(playerData.group, self.flights) - if flight then - local stack=flight.flag:Get() - local stackalt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) - text=text..string.format("Aircraft: %s\n", flight.actype) - text=text..string.format("Flag/stack: %d\n", stack) - text=text..string.format("Stack alt: %d ft\n", stackalt) - text=text..string.format("# units: %s\n", flight.nunits) - text=text..string.format("# section: %s", #flight.section) - for _,_sec in pairs(flight.section) do - local sec=_sec --#AIRBOSS.Flightitem - text=text..string.format("\n- %s", sec.player.name) - end - else - text=text..string.format("Your flight is not registered in CCA.") + text=text..string.format("# units: %d\n", #playerData.group:GetUnits()) + text=text..string.format("n units: %d\n", playerData.nunits) + text=text..string.format("Section Lead: %s\n", tostring(playerData.seclead)) + text=text..string.format("# section: %d", #playerData.section) + for _,_sec in pairs(playerData.section) do + local sec=_sec --#AIRBOSS.PlayerData + text=text..string.format("\n- %s", sec.name) end if playerData.step==AIRBOSS.PatternStep.INITIAL then + -- Heading and distance to initial zone. local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate())) local brc=self:GetBRC() - - + + -- Help player to find its way to the initial zone. text=text..string.format("\nFly heading %03d° for %.1f NM and turn to BRC %03d°.", flyhdg, flydist, brc) + + elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then + + -- Heading and distance to platform zone. + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self.zonePlatform:GetCoordinate()) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate())) + local fb=self:GetFinalBearing(true) + + -- Help player to find its way to the initial zone. + text=text..string.format("\nFly heading %03d° for %.1f NM and turn to FB %03d°.", flyhdg, flydist, fb) + end - MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", 30, true) end end end ---- Smoke current marshal zone of player. +--- Mark current marshal zone of player by either smoke or flares. -- @param #AIRBOSS self -- @param #string _unitName Name of the player unit. -function AIRBOSS:_SmokeMarshalZone(_unitName) +-- @param #boolean flare If true, flare the zone. If false, smoke the zone. +function AIRBOSS:_MarkMarshalZone(_unitName, flare) -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) @@ -5321,19 +5548,32 @@ function AIRBOSS:_SmokeMarshalZone(_unitName) local text="No marshal zone to smoke!" if zone then - text="Smoking marshal zone with GREEN smoke." - zone:SmokeZone(SMOKECOLOR.Green) + + --TODO: Add height! + + if flare then + text="Marking marshal zone with WHITE flares." + zone:FlareZone(FLARECOLOR.White, 45) + else + text="Marking marshal zone with WHITE smoke." + zone:SmokeZone(SMOKECOLOR.White, 45) + end + end - MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) + + -- Send message to player. + self:MessageToPlayer(playerData, text, "AIRBOSS", "", 10) end end end ---- Flare current marshal zone of player. + +--- Mark current marshal zone of player by either smoke or flares. -- @param #AIRBOSS self -- @param #string _unitName Name of the player unit. -function AIRBOSS:_FlareMarshalZone(_unitName) +-- @param #boolean flare If true, flare the zone. If false, smoke the zone. +function AIRBOSS:_MarkCase23Zones(_unitName, flare) -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) @@ -5344,20 +5584,39 @@ function AIRBOSS:_FlareMarshalZone(_unitName) if playerData then - -- Get current holding zone. - local zone=self:_GetHoldingZone(playerData) + local text="CASE II/III: Marking:\n" - local text="No marshal zone to flare!" - if zone then - text="Flaring marshal zone with GREEN flares." - zone:FlareZone(FLARECOLOR.Green, 90) + --TODO: Add height! + + if flare then + text=text.."* Valid zone with GREEN flares\n" + local zp=self:_GetCase23ValidZone() + zp:FlareZone(FLARECOLOR.Green, 45) + text=text.."* Platform zone with RED flares\n" + self.zonePlatform:FlareZone(FLARECOLOR.Red, 45) + text=text.."* Dirty up zone with YELLOW flares\n" + self.zoneDirtyup:FlareZone(FLARECOLOR.Yellow, 45) + text=text.."* Bullseye zone with WHITE flares\n" + self.zoneBullseye:FlareZone(FLARECOLOR.White, 45) + else + text=text.."* Valid zone with GREEN smoke\n" + local zp=self:_GetCase23ValidZone() + zp:SmokeZone(SMOKECOLOR.Green, 45) + text=text.."* Platform zone with RED smoke\n" + self.zonePlatform:SmokeZone(SMOKECOLOR.Red, 45) + text=text.."* Dirty up zone with ORANGE flares\n" + self.zoneDirtyup:SmokeZone(SMOKECOLOR.Orange, 45) + text=text.."* Bullseye zone with BLUE smoke\n" + self.zoneBullseye:SmokeZone(SMOKECOLOR.Blue, 45) end - MESSAGE:New(text, 20, nil, true):ToClient(playerData.client) + -- Send message to player. + self:MessageToPlayer(playerData, text, "AIRBOSS", "", 10) end end end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ From 6b24673b6f5ce238d3cc74d1038c9a6d9b5057c0 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Mon, 26 Nov 2018 16:35:35 +0100 Subject: [PATCH 060/147] AIRBOSS v0.3.5w --- Moose Development/Moose/Ops/Airboss.lua | 349 +++++++++--------- .../Moose/Ops/RecoveryTanker.lua | 254 +++++++++---- Moose Development/Moose/Ops/RescueHelo.lua | 10 +- 3 files changed, 376 insertions(+), 237 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 58db6086d..f947fb967 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -432,6 +432,7 @@ AIRBOSS.GroovePos={ -- @field #string onboard Onboard number. -- @field #string difficulty Difficulty level. -- @field #string step Coming pattern step. +-- @field #boolean warning Set true once the player got a warning. -- @field #number passes Number of passes. -- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. -- @field #table debrief Debrief analysis of the current step of this pass. @@ -455,7 +456,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.5" +AIRBOSS.version="0.3.5w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1211,6 +1212,8 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) alt=UTILS.FeetToMeters(1200) dist=-UTILS.NMToMeters(3) + + aoa=8.1 elseif step==AIRBOSS.PatternStep.INITIAL then @@ -1226,9 +1229,11 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) if hornet then alt=UTILS.FeetToMeters(800) + speed=UTILS.KnotsToMps(350) elseif skyhawk then - alt=UTILS.FeetToMeters(600) - end + alt=UTILS.FeetToMeters(600) + speed=UTILS.KnotsToMps(250) + end elseif step==AIRBOSS.PatternStep.EARLYBREAK then @@ -1971,6 +1976,7 @@ function AIRBOSS:_InitPlayer(playerData) playerData.step=AIRBOSS.PatternStep.UNDEFINED playerData.groove={} playerData.debrief={} + playerData.warning=nil playerData.holding=nil playerData.lig=false playerData.patternwo=false @@ -2123,18 +2129,19 @@ end --- Remove a flight from all queues. Also set player step to undefined if applicable. -- @param #AIRBOSS self --- @param #AIRBOSS.Flightitem flight The flight to be removed. +-- @param #AIRBOSS.PlayerData flight The flight to be removed. function AIRBOSS:_RemoveFlight(flight) -- Remove flight from all queues. self:_RemoveFlightFromQueue(self.Qmarshal, flight) self:_RemoveFlightFromQueue(self.Qpattern, flight) - -- Set Playerstep to undefined. + -- Check if player or AI if flight.player then + -- Set Playerstep to undefined. flight.step=AIRBOSS.PatternStep.UNDEFINED else - -- Remove flight completely + -- Remove AI flight completely. self:_RemoveFlightFromQueue(self.flights, flight) end @@ -2668,10 +2675,10 @@ function AIRBOSS:_Commencing(playerData) self:_InitPlayer(playerData) -- Commence - local text=string.format("Commencing. Case %d.", self.case) + local text=string.format("Commencing. (Case %d)", self.case) - -- Message to player. - self:_SendMessageToPlayer(text, 10, playerData) + -- Message to all players. + self:MessageToAll(text, playerData.onboard, "", 5) -- Next step: depends on case recovery. if self.case==1 then @@ -2694,11 +2701,11 @@ function AIRBOSS:_Initial(playerData) -- Inform player. local hint=string.format("Entering the pattern.") if playerData.difficulty==AIRBOSS.Difficulty.EASY then - hint=hint.."Aim for 800 feet and 350 kts at the break entry." + hint=hint.."\nAim for 800 feet and 350 kts at the break entry." end -- Send message. - self:_SendMessageToPlayer(hint, 10, playerData) + self:MessageToPlayer(playerData, hint, "MARSHAL") -- Next step: upwind. playerData.step=AIRBOSS.PatternStep.UPWIND @@ -2722,23 +2729,6 @@ function AIRBOSS:_Descent4k(playerData) -- Check if we are in front of the boat (diffX > 0). if self:_CheckLimits(X, Z, self.Descent4k) then - - -- Get optimal altitude, distance and speed. - local altitude=self:_GetAircraftParameters(playerData) - - -- TODO: only speed is checked here! - - MESSAGE:New("Descent 4k step reached", 5):ToAllIf(self.Debug) - - -- Get altitude. - local hint, debrief=self:_AltitudeCheck(playerData, altitude) - - -- Message to player - self:_SendMessageToPlayer(hint, 10, playerData) - - -- Debrief. - self:_AddToSummary(playerData, "Descent 4k", debrief) - -- Next step: Platform at 5k playerData.step=AIRBOSS.PatternStep.PLATFORM end @@ -2748,43 +2738,46 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Platform(playerData) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi=self:_GetDistances(playerData.unit) - -- Abort condition check. - if self:_CheckAbort(X, Z, self.Platform) then - self:_AbortPattern(playerData, X, Z, self.Platform) - --return + -- Check if player is in valid zone + local validzone=self:_GetCase23ValidZone() + + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and not playerData.warning then + self:MessageToPlayer(playerData, "You left the valid pattern zone!", "AIRBOSS") + playerData.warning=true end - + -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self.zonePlatform) -- Check if we are in zone. if inzone then + -- Debug message. MESSAGE:New("Platform step reached", 5):ToAllIf(self.Debug) -- Get optimal altitiude. local altitude, aoa, distance, speed =self:_GetAircraftParameters(playerData) - - --TODO: check speed. -- Get altitude hint. - local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, altitude) + local hintAlt=self:_AltitudeCheck(playerData, altitude) -- Get altitude hint. - local hintSpeed, debriefSpeed=self:_AltitudeCheck(playerData, altitude) + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end - -- Message to player - self:_SendMessageToPlayer(hint, 10, playerData) - - -- Debrief. - --self:_AddToSummary(playerData, "Platform 5k", debrief) - -- Next step: Dirty up and level out at 1200 ft. playerData.step=AIRBOSS.PatternStep.DIRTYUP + playerData.warning=nil end end @@ -2793,13 +2786,16 @@ end -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_DirtyUp(playerData) - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi=self:_GetDistances(playerData.unit) + -- Check if player is in valid zone + local validzone=self:_GetCase23ValidZone() - -- Abort condition check. - if self:_CheckAbort(X, Z, self.DirtyUp) then - self:_AbortPattern(playerData, X, Z, self.DirtyUp) - --return + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and not playerData.warning then + self:MessageToPlayer(playerData, "You left the valid pattern zone!", "AIRBOSS") + playerData.warning=true end -- Check if we are inside the moving zone. @@ -2808,30 +2804,34 @@ function AIRBOSS:_DirtyUp(playerData) --if self:_CheckLimits(X, Z, self.DirtyUp) then if inzone then + -- Debug message. MESSAGE:New("Dirty up step reached", 5):ToAllIf(self.Debug) - - --TODO: speed check -- Get optimal altitiude. - local altitude=self:_GetAircraftParameters(playerData) + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) - -- Get altitude. - local hint, debrief=self:_AltitudeCheck(playerData, altitude) + -- Get altitude hint. + local hintAlt, debrief=self:_AltitudeCheck(playerData, altitude) + + -- Get speed hint. + -- TODO: Not sure if we already need to be onspeed AoA at this point? + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end - -- Message to player - self:_SendMessageToPlayer(hint, 10, playerData) - - -- Debrief. - self:_AddToSummary(playerData, "Dirty Up", debrief) - -- Next step: if self.case==2 then -- CASE II: Fly to the initial and perform CASE I pattern. - playerData.step=AIRBOSS.PatternStep.INITIAL + playerData.step=AIRBOSS.PatternStep.INITIAL elseif self.case==3 then -- CASE III: Intercept glide slope and follow bullseye (ICLS). playerData.step=AIRBOSS.PatternStep.BULLSEYE end + playerData.warning=nil end end @@ -2840,13 +2840,16 @@ end -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Bullseye(playerData) - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi=self:_GetDistances(playerData.unit) + -- Check if player is in valid zone + local validzone=self:_GetCase23ValidZone() - -- Abort condition check. - if self:_CheckAbort(X, Z, self.Bullseye) then - self:_AbortPattern(playerData, X, Z, self.Bullseye) - --return + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and not playerData.warning then + self:MessageToPlayer(playerData, "You left the valid pattern zone!", "AIRBOSS") + playerData.warning=true end -- Check if we are inside the moving zone. @@ -2856,24 +2859,27 @@ function AIRBOSS:_Bullseye(playerData) --if self:_CheckLimits(X, Z, self.Bullseye) then if inzone then + -- Debug message. MESSAGE:New("Bullseye step reached", 5):ToAllIf(self.Debug) -- Get optimal altitiude. - local altitude=self:_GetAircraftParameters(playerData) + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) - -- Get altitude. - local hint, debrief=self:_AltitudeCheck(playerData, altitude) - - -- Message to player - self:_SendMessageToPlayer(hint, 10, playerData) + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, altitude) + + -- Get altitude hint. + local hintAoA=self:_AoACheck(playerData, aoa) - -- Debrief. - self:_AddToSummary(playerData, "Bullseye", debrief) + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end - -- Next step: Final approach in the groove. - --playerData.step=AIRBOSS.PatternStep.FINAL -- Next step: Groove Call the ball. playerData.step=AIRBOSS.PatternStep.GROOVE_XX + playerData.warning=nil end end @@ -2898,14 +2904,20 @@ function AIRBOSS:_Upwind(playerData) -- Get optimal altitude, distance and speed. local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) - -- Get altitude. - local hint, debrief=self:_AltitudeCheck(playerData, alt) - - -- Message to player - self:_SendMessageToPlayer(hint, 10, playerData) + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, alt) + -- Get speed hint. + local hintSpeed=self:_AltitudeCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + -- Debrief. - self:_AddToSummary(playerData, "Entering the Break", debrief) + --self:_AddToSummary(playerData, "Entering the Break", debrief) -- Next step: Early Break. playerData.step=AIRBOSS.PatternStep.EARLYBREAK @@ -2943,8 +2955,11 @@ function AIRBOSS:_Break(playerData, part) -- Grade altitude. local hint, debrief=self:_AltitudeCheck(playerData, altitude) - -- Send message to player. - self:_SendMessageToPlayer(hint, 10, playerData) + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s %s", playerData.step, hint) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end -- Debrief if part=="early" then @@ -2980,7 +2995,7 @@ function AIRBOSS:_CheckForLongDownwind(playerData) self:RadioTransmission(self.LSOradio, self.radiocall.LONGINGROOVE) -- Debrief. - self:_AddToSummary(playerData, "Downwind", "Long in the groove.") + self:_AddToSummary(playerData, "Downwind", "Long in the groove - Pattern Wave Off!") --grade="LIG PATTERN WAVE OFF - CUT 1 PT" playerData.lig=true @@ -3021,13 +3036,19 @@ function AIRBOSS:_Abeam(playerData) -- Grade distance to carrier. local hintDist, debriefDist=self:_DistanceCheck(playerData, dist) --math.abs(Z) - -- Compile full hint. - local hint=string.format("%s\n%s\n%s", hintAlt, hintAoA, hintDist) + -- Paddles contact. + -- TODO: radio message. + self:MessageToPlayer(playerData, "Paddles, contact.", "LSO", "") + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s\n%s", playerData.step, hintAlt, hintAoA, hintDist) + self:MessageToPlayer(playerData, hint, "LSO", "") + end + + -- Compile full hint. local debrief=string.format("%s\n%s\n%s", debriefAlt, debriefAoA, debriefDist) - - -- Send message to playerr. - self:_SendMessageToPlayer(hint, 10, playerData) - + -- Add to debrief. self:_AddToSummary(playerData, "Abeam Position", debrief) @@ -3064,13 +3085,15 @@ function AIRBOSS:_Ninety(playerData) -- Grade AoA. local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) - - -- Compile full hint. - local hint=string.format("%s\n%s", hintAlt, hintAoA) - local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) - + -- Message to player. - self:_SendMessageToPlayer(hint, 10, playerData) + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + self:MessageToPlayer(playerData, hint, "LSO", "") + end + + -- Debrief. + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) -- Add to debrief. self:_AddToSummary(playerData, "At the 90", debrief) @@ -3080,7 +3103,7 @@ function AIRBOSS:_Ninety(playerData) elseif relheading>90 and self:_CheckLimits(X, Z, self.Wake) then -- Message to player. - self:_SendMessageToPlayer("You are already at the wake and have not passed the 90! Turn faster next time!", 10, playerData) + self:MessageToPlayer(playerData, "You are already at the wake and have not passed the 90! Turn faster next time!", "LSO", "") --TODO: pattern WO? end end @@ -3111,12 +3134,14 @@ function AIRBOSS:_Wake(playerData) -- Grade AoA. local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) - -- Compile full hint. - local hint=string.format("%s\n%s", hintAlt, hintAoA) - local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) - -- Message to player. - self:_SendMessageToPlayer(hint, 10, playerData) + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + self:MessageToPlayer(playerData, hint, "LSO", "") + end + + -- Debrief. + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) -- Add to debrief. self:_AddToSummary(playerData, "At the Wake", debrief) @@ -3161,14 +3186,14 @@ function AIRBOSS:_Final(playerData) -- AoA feed back local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) - -- Compile full hint. - local hint=string.format("%s\n%s", hintAlt, hintAoA) - local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) - -- Message to player. - self:_SendMessageToPlayer(hint, 10, playerData) + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + self:MessageToPlayer(playerData, hint, "LSO", "") + end -- Add to debrief. + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) self:_AddToSummary(playerData, "Enter Groove", debrief) -- Gather pilot data. @@ -3266,7 +3291,8 @@ function AIRBOSS:_Groove(playerData) elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then -- Debug. - self:_SendMessageToPlayer("IM", 5, playerData) + local text=string.format("FF IM=%d", rho) + MESSAGE:New(text, 5):ToAllIf(self.Debug) self:I(self.lid..string.format("FF IM=%d", rho)) -- Store data. @@ -3281,8 +3307,9 @@ function AIRBOSS:_Groove(playerData) if playerData.waveoff==false then -- Debug - self:_SendMessageToPlayer("IC", 5, playerData) - self:I(self.lid..string.format("FF IC=%d", rho)) + local text=string.format("FF IC=%d", rho) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:I(self.lid..text) -- Store data. playerData.groove.IC=groovedata @@ -3311,9 +3338,10 @@ function AIRBOSS:_Groove(playerData) elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AR then -- Debug. - self:_SendMessageToPlayer("AR", 8, playerData) - self:I(self.lid..string.format("FF AR=%d", rho)) - + local text=string.format("FF AR=%d", rho) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:I(self.lid..text) + -- Store data. playerData.groove.AR=groovedata @@ -3435,14 +3463,16 @@ function AIRBOSS:_Trapped(playerData, X) -- Info to player. local text=string.format("Trapped! %d-wire.", wire) - self:_SendMessageToPlayer(text, 10, playerData) + self:MessageToPlayer(playerData, text, "LSO", "") - local text2=string.format("Distance X=%.1f meters resulted in a %d-wire estimate.", X, wire) - MESSAGE:New(text,30):ToAllIf(self.Debug) - self:I(self.lid..text2) + -- Debug message. + local text=string.format("Distance X=%.1f meters resulted in a %d-wire estimate.", X, wire) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:I(self.lid..text) + -- Debrief. local hint = string.format("Trapped catching the %d-wire.", wire) - self:_AddToSummary(playerData, "Recovered", hint) + self:_AddToSummary(playerData, "Goove: IW", hint) else --Boltered! @@ -3868,11 +3898,9 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) text=text.."Unknown AoA state." end + -- Text not used. text=text..string.format(" AoA = %.1f", aoa) - -- LSO Message to player. - --self:_SendMessageToPlayer(text, 5, playerData, false) - -- Set last time. playerData.Tlso=timer.getTime() end @@ -4266,13 +4294,13 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) local hint if _error>badscore then - hint=string.format("You're high. ") + hint=string.format("You're high.") elseif _error>lowscore then - hint= string.format("You're slightly high. ") + hint= string.format("You're slightly high.") elseif _error<-badscore then hint=string.format("You're low. ") elseif _error<-lowscore then - hint=string.format("You're slightly low. ") + hint=string.format("You're slightly low.") else hint=string.format("Good altitude. ") end @@ -4315,15 +4343,15 @@ function AIRBOSS:_DistanceCheck(playerData, optdist) local hint if _error>badscore then - hint=string.format("You're too far from the boat! ") + hint=string.format("You're too far from the boat!") elseif _error>lowscore then - hint=string.format("You're slightly too far from the boat. ") + hint=string.format("You're slightly too far from the boat.") elseif _error<-badscore then - hint=string.format( "You're too close to the boat! ") + hint=string.format( "You're too close to the boat!") elseif _error<-lowscore then - hint=string.format("You're slightly too far from the boat. ") + hint=string.format("You're slightly too far from the boat.") else - hint=string.format("Perfect distance to the boat. ") + hint=string.format("Good distance to the boat.") end -- Extend or decrease depending on skill. @@ -4583,10 +4611,7 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay) radio:Broadcast(true) -- "Subtitle". - for _,_player in pairs(self.players) do - local playerData=_player --#AIRBOSS.PlayerData - self:_SendMessageToPlayer(call.subtitle, call.duration, playerData) - end + self:MessageToAll(call.subtitle, radio:GetAlias(), "", call.duration) else @@ -4604,34 +4629,6 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay) end end ---- Send message to player client. --- @param #AIRBOSS self --- @param #string message The message to send. --- @param #number duration Display message duration. --- @param #AIRBOSS.PlayerData playerData Player data. --- @param #boolean clear If true, clear screen from previous messages. --- @param #string sender The person who sends the message. Default is carrier alias. --- @param #number delay Delay in seconds, before the message is send. -function AIRBOSS:_SendMessageToPlayer(message, duration, playerData, clear, sender, delay) - - if playerData and message and message~="" then - - -- Format message. - local text=string.format("%s, %s", playerData.callsign, message) - self:I(self.lid..text) - - if delay and delay>0 then - SCHEDULER:New(nil, self._SendMessageToPlayer, {self, message, duration, playerData, clear, sender}, delay) - else - if playerData.client then - MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) - end - end - - end - -end - --- Send text message to player client. -- Message format will be "SENDER: RECCEIVER, MESSAGE". -- @param #AIRBOSS self @@ -4671,6 +4668,26 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration end +--- Send text message to all players in the CCA. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay) + + for _,_player in pairs(self.players) do + local player=_player --#AIRBOSS.PlayerData + if player.unit:IsInZone(self.zoneCCA) then + self:MessageToPlayer(player,message,sender,receiver,duration,clear,delay) + end + end + +end + --- Check if aircraft is capable of landing on an aircraft carrier. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 2b4149dca..54068e7fb 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -35,7 +35,9 @@ -- @field #number altitude Tanker orbit pattern altitude. -- @field #number distStern Race-track distance astern. -- @field #number distBow Race-track distance bow. --- @field #number dTupdate Time interval for updating pattern position wrt new tanker position. +-- @field #number Dupdate Pattern update when carrier changes its position by more than this distance (meters). +-- @field #number Hupdate Pattern update when carrier changes its heading by more than this number (degrees). +-- @field #number dTupdate Minimum time interval in seconds before the next pattern update can happen. -- @field #number Tupdate Last time the pattern was updated. -- @field #number takeoff Takeoff type (cold, hot, air). -- @field #number lowfuel Low fuel threshold in percent. @@ -58,14 +60,14 @@ -- -- # Simple Script -- --- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named "USS Stennis". +-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named **"USS Stennis"**. -- --- Secondly, you need to define a recovery tanker group in the mission editor and set it to "LATE ACTIVATED". The name of the group we'll use is "Texaco". +-- Secondly, you need to define a recovery tanker group in the mission editor and set it to **"LATE ACTIVATED"**. The name of the group we'll use is **"Texaco"**. -- --- The basic script is very simple and consists of only two lines. +-- The basic script is very simple and consists of only two lines: -- --- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") --- TexacoStennis:Start() +-- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") +-- TexacoStennis:Start() -- -- The first line will create a new RECOVERYTANKER object and the second line starts the process. -- @@ -78,11 +80,11 @@ -- -- Once the tanker runs out of fuel itself, it will return to the carrier and be respawned. -- --- # Fine Tuning +-- # Options and Fine Tuning -- -- Several parameters can be customized by the mission designer. -- --- ## Adjusting the Takeoff Type +-- ## Takeoff Type -- -- By default, the tanker is spawned with running engies on the carrier. The mission designer has set option to set the take off type via the @{#RECOVERYTANKER.SetTakeoff} function. -- Or via shortcuts @@ -92,9 +94,9 @@ -- * @{#RECOVERYTANKER.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the tanker will be spawned in air relatively far behind the carrier. -- -- For example, --- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") --- TexacoStennis:SetTakeoffAir() --- TexacoStennis:Start() +-- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") +-- TexacoStennis:SetTakeoffAir() +-- TexacoStennis:Start() -- will spawn the tanker several nautical miles astern the carrier. From there it will start its pattern. -- -- Spawning in air is not as realsitic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck. @@ -104,9 +106,9 @@ -- If only the first spawning should happen on the carrier, one use the @{#RECOVERYTANKER.SetRespawnInAir}() function to command that all subsequent spawning -- will happen in air. -- --- If the helo should no be respawned at all, one can set @{#RECOVERYTANKER.SetRespawnOff}(). +-- If the helo should not be respawned at all, one can set @{#RECOVERYTANKER.SetRespawnOff}(). -- --- ## Adjusting the Pattern +-- ## Pattern Parameters -- -- The racetrack pattern parameters can be fine tuned via the following functions: -- @@ -114,10 +116,69 @@ -- * @{#RECOVERYTANKER.SetSpeed}(*speed*), where *speed* is the pattern speed in knots. Default is 272 knots. -- * @{#RECOVERYTANKER.SetRacetrackDistances}(*distbow*, *diststern*), where *distbow* and *diststern* are the distances ahead and astern the boat, respectively. -- +-- ## TACAN +-- +-- A TACAN beacon for the tanker can be activated via scripting, i.e. no need to do this within the mission editor. +-- +-- The beacon is create with the @{#RECOVERYTANKER.SetTACAN}(*channel*, *mode*, *morse*) function, where *channel* is the TACAN channel (a number), *mode* the TACAN mode (either "X" +-- or "Y") and *morse* a three letter string that is send as morse code to identify the tanker: +-- +-- TexacoStennis:SetTACAN(10, "Y", "TKR") +-- +-- will activate a TACAN beacon 10Y with more code "TKR". +-- +-- If you do not set a TACAN beacon explicitly, it is automatically create on channel 1, mode "Y" and morse code "TKR". +-- +-- In order to completely disable the TACAN beacon, you can use the @{#RECOVERYTANKER.SetTACANoff}() function in your script. +-- +-- Note to self, I am not sure, if an AA TACAN station *must* be of mode "Y" in order to work. It seems that this was the case in earlier DCS versions. +-- +-- ## Pattern Update +-- +-- The pattern of the tanker is updated if at least one of the two following conditions apply: +-- +-- * The aircraft carrier changes its position by more than ~10 km (see @{#RECOVERYTANKER.SetPatternUpdateDistance}) and/or +-- * The aircraft carrier changes its heading by more than 5 degrees (see @{#RECOVERYTANKER.SetPatternUpdateHeading}) +-- +-- **Note** that updating the pattern always leads to a small disruption in the perfect racetrack pattern of the tanker. This is because a new waypoint and new racetrack points +-- need to be set as DCS task. This is also the reason why the pattern is not contantly updated but rather when the position or heading of the carrier changes significantly. +-- +-- The maximum update frequency is set to 15 minutes. You can adjust this by @{#RECOVERYTANKER.SetPatternUpdateInterval}. +-- +-- # Finite State Model +-- +-- The implementation uses a Finite State Model (FSM). This allows the mission designer to hook in to certain events. +-- +-- * @{#RECOVERYTANKER.Start}: This event starts the FMS process and initialized parameters and spawns the tanker. DCS event handling is started. +-- * @{#RECOVERYTANKER.Status}: This event is called in regular intervals (~60 seconds) and checks the status of the tanker and carrier. It triggers other events if necessary. +-- * @{#RECOVERYTANKER.PatternUpdate}: This event commands the tanker to update its pattern +-- * @{#RECOVERYTANKER.RTB}: This events sends the tanker to its home base (usually the carrier). This is called once the tanker runs low on gas. +-- * @{#RECOVERYTANKER.RefuelStart}: This event is called when a tanker starts to refuel another unit. +-- * @{#RECOVERYTANKER.RefuelStop}: This event is called when a tanker stopped to refuel another unit. +-- * @{#RECOVERYTANKER.Run}: This event is called when the tanker resumes normal operations, e.g. after refueling stopped or tanker finished refueling. +-- * @{#RECOVERYTANKER.Stop}: This event stops the FSM by unhandling DCS events. +-- +-- The mission designer can capture these events by RECOVERYTANKER.OnAfter*Eventname* functions, e.g. @{#RECOVERYTANKER.OnAfterPatternUpdate}. +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#RECOVERYTANKER} class should have the string "RECOVERYTANKER" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("RECOVERYTANKER") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- -- @field #RECOVERYTANKER RECOVERYTANKER = { ClassName = "RECOVERYTANKER", - Debug = true, + Debug = false, carrier = nil, carriertype = nil, tankergroupname = nil, @@ -133,6 +194,8 @@ RECOVERYTANKER = { distStern = nil, distBow = nil, dTupdate = nil, + Dupdate = nil, + Hupdate = nil, Tupdate = nil, takeoff = nil, lowfuel = nil, @@ -145,16 +208,18 @@ RECOVERYTANKER = { --- Class version. -- @field #string version -RECOVERYTANKER.version="0.9.4" +RECOVERYTANKER.version="0.9.4w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Check if TACAN mode "X" is allowed for AA TACAN stations. +-- TODO: Check if tanker is going back to "Running" state after RTB and respawn. -- TODO: Is alive check for tanker. --- TODO: Trace functions self:T instead of self:I for less output. --- TODO: Make pattern update parameters (distance, orientation) input parameters. --- TODO: Write documenation. +-- DONE: Write documenation. +-- DONE: Trace functions self:T instead of self:I for less output. +-- DONE: Make pattern update parameters (distance, orientation) input parameters. -- DONE: Add FSM event for pattern update. -- DONE: Smarter pattern update function. E.g. (small) zone around carrier. Only update position when carrier leaves zone or changes heading? -- DONE: Set AA TACAN. @@ -191,7 +256,6 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self.carrier:SetState(self.carrier, "RECOVERYTANKER", self) -- Init default parameters. - self:SetPatternUpdateInterval() self:SetAltitude() self:SetSpeed() self:SetRacetrackDistances(6, 8) @@ -200,6 +264,9 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetLowFuelThreshold() self:SetRespawnOnOff() self:SetTACAN() + self:SetPatternUpdateDistance() + self:SetPatternUpdateHeading() + self:SetPatternUpdateInterval() ----------------------- --- FSM Transitions --- @@ -211,7 +278,8 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") -- Start the FSM. - self:AddTransition("*", "Refuel", "Refueling") -- Tanker starts to refuel. + self:AddTransition("*", "RefuelStart", "Refueling") -- Tanker has started to refuel another unit. + self:AddTransition("*", "RefuelStop", "Running") -- Tanker starts to refuel. self:AddTransition("*", "Run", "Running") -- Tanker starts normal operation again. self:AddTransition("Running", "RTB", "Returning") -- Tanker is returning to base (for fuel). self:AddTransition("*", "Status", "*") -- Status update. @@ -229,23 +297,39 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- @param #number delay Delay in seconds. - --- Triggers the FSM event "Refuel" when the tanker is refueling another aircraft. - -- @function [parent=#RECOVERYTANKER] Refuel + --- Triggers the FSM event "RefuelStart" when the tanker starts refueling another aircraft. + -- @function [parent=#RECOVERYTANKER] RefuelStart -- @param #RECOVERYTANKER self -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. - --- Triggers delayed the FSM event "Refuel" when the tanker is refueling another aircraft. - -- @function [parent=#RECOVERYTANKER] __Refuel + --- On after "RefuelStart" event user function. Called when a the the tanker started to refuel another unit. + -- @function [parent=#RECOVERYTANKER] OnAfterRefuelStart -- @param #RECOVERYTANKER self - -- @param #number delay Delay in seconds. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. - --- Triggers the FSM event "Run". Simply puts the group into "Running" state, e.g. after refueling ended. + --- Triggers the FSM event "RefuelStop" when the tanker stops refueling another aircraft. + -- @function [parent=#RECOVERYTANKER] RefuelStop + -- @param #RECOVERYTANKER self + -- @param Wrapper.Unit#UNIT receiver Unit stoped receiving fuel from the tanker. + + --- On after "RefuelStop" event user function. Called when a the the tanker stopped to refuel another unit. + -- @function [parent=#RECOVERYTANKER] OnAfterRefuelStop + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT receiver Unit that received fuel from the tanker. + + + --- Triggers the FSM event "Run". Simply puts the group into "Running" state. -- @function [parent=#RECOVERYTANKER] Run -- @param #RECOVERYTANKER self - --- Triggers delayed the FSM event "Run". Simply puts the group into "Running" state, e.g. after refueling ended. + --- Triggers delayed the FSM event "Run". Simply puts the group into "Running" state. -- @function [parent=#RECOVERYTANKER] __Run -- @param #RECOVERYTANKER self -- @param #number delay Delay in seconds. @@ -262,6 +346,14 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- @param #number delay Delay in seconds. -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + --- On after "RTB" event user function. Called when a the the tanker returns to its home base. + -- @function [parent=#RECOVERYTANKER] OnAfterPatternUpdate + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + --- Triggers the FSM event "Status" that updates the tanker status. -- @function [parent=#RECOVERYTANKER] Status @@ -282,6 +374,13 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- @param #RECOVERYTANKER self -- @param #number delay Delay in seconds. + --- On after "PatternEvent" event user function. Called when a the pattern of the tanker is updated. + -- @function [parent=#RECOVERYTANKER] OnAfterPatternUpdate + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + --- Triggers the FSM event "Stop" that stops the recovery tanker. Event handlers are stopped. -- @function [parent=#RECOVERYTANKER] Stop @@ -328,22 +427,39 @@ function RECOVERYTANKER:SetRacetrackDistances(distbow, diststern) return self end ---- Set pattern update interval. Note that this update causes a slight disruption in the race track pattern. --- Therefore, the interval should be as long as possible but short enough to keep the tanker overhead the carrier. +--- Set minimum pattern update interval. After a pattern update this time interval has to pass before the next update is allowed. -- @param #RECOVERYTANKER self --- @param #number interval Interval in minutes. Default is every 30 minutes. +-- @param #number interval Min interval in minutes. Default is 15 minutes. -- @return #RECOVERYTANKER self function RECOVERYTANKER:SetPatternUpdateInterval(interval) - self.dTupdate=(interval or 30)*60 + self.dTupdate=(interval or 15)*60 + return self +end + +--- Set pattern update distance. Tanker will update its pattern when the carrier changes its position by more than this distance. +-- @param #RECOVERYTANKER self +-- @param #number distancechange Distance threshold in km. Default 9.62 km (= 5 NM). +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateDistance(distancechange) + self.Dupdate=(distancechange or 9.62)*1000 + return self +end + +--- Set pattern update heading. Tanker will update its pattern when the carrier changes its heading by more than this value. +-- @param #RECOVERYTANKER self +-- @param #number headingchange Heading threshold in degrees. Default 5 degrees. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateHeading(headingchange) + self.Hupdate=headingchange or 5 return self end --- Set low fuel state of tanker. When fuel is below this threshold, the tanker will RTB or be respawned if takeoff type is in air. -- @param #RECOVERYTANKER self --- @param #number threshold Low fuel threshold in percent. Default 10. +-- @param #number fuelthreshold Low fuel threshold in percent. Default 10 %. -- @return #RECOVERYTANKER self -function RECOVERYTANKER:SetLowFuelThreshold(threshold) - self.lowfuel=threshold or 10 +function RECOVERYTANKER:SetLowFuelThreshold(fuelthreshold) + self.lowfuel=fuelthreshold or 10 return self end @@ -381,7 +497,7 @@ function RECOVERYTANKER:SetTakeoffCold() return self end ---- Set takeoff in air at pattern altitude 30 NM behind the carrier. +--- Set takeoff in air at the defined pattern altitude and 20 NM astern the carrier. -- @param #RECOVERYTANKER self -- @return #RECOVERYTANKER self function RECOVERYTANKER:SetTakeoffAir() @@ -389,7 +505,6 @@ function RECOVERYTANKER:SetTakeoffAir() return self end - --- Enable respawning of tanker. Note that this is the default behaviour. -- @param #RECOVERYTANKER self -- @return #RECOVERYTANKER self @@ -483,6 +598,13 @@ function RECOVERYTANKER:IsRefueling() return self:is("Refueling") end +--- Check if FMS was stopped. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, is stopped. +function RECOVERYTANKER:IsStopped() + return self:is("Stopped") +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM states ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -524,8 +646,6 @@ function RECOVERYTANKER:onafterStart(From, Event, To) -- Spawn at coordinate. self.tanker=Spawn:SpawnFromCoordinate(Carrier) - -- Initial route. - self:_InitRoute(15, 1) else -- Check if an uncontrolled tanker group was requested. @@ -552,10 +672,10 @@ function RECOVERYTANKER:onafterStart(From, Event, To) end - -- Initialize route. - self:_InitRoute(15, 1) - end + + -- Initialize route. + self:_InitRoute(15, 1) -- Create tanker beacon. if self.TACANon then @@ -566,7 +686,7 @@ function RECOVERYTANKER:onafterStart(From, Event, To) self.orientation=self.carrier:GetOrientationX() self.position=self.carrier:GetCoordinate() - -- Init status check. + -- Init status updates in 10 seconds. self:__Status(10) end @@ -584,7 +704,7 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) -- Get fuel of tanker. local fuel=self.tanker:GetFuel()*100 local text=string.format("Recovery tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) - self:I(text) + self:T(text) -- Check if tanker is running and not RTBing or refueling. if self:IsRunning() then @@ -599,8 +719,8 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) if self.respawn then -- Debug message. - local text=string.format("Respawning tanker %s.", self.tanker:GetName()) - self:I(text) + local text=string.format("Respawning recovery tanker %s in air.", self.tanker:GetName()) + self:T(text) -- Respawn tanker. self.tanker:InitHeading(self.tanker:GetHeading()) @@ -643,7 +763,9 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) end -- Call status again in 1 minute. - self:__Status(-60) + if not self:IsStopped() then + self:__Status(-60) + end end --- On after "PatternUpdate" event. Updates the racetrack pattern of the tanker wrt the carrier position. @@ -654,7 +776,7 @@ end function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) -- Debug message. - self:I(string.format("Updating recovery tanker %s orbit.", self.tanker:GetName())) + self:T(string.format("Updating recovery tanker %s orbit.", self.tanker:GetName())) -- Carrier heading. local hdg=self.carrier:GetHeading() @@ -675,7 +797,6 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) p0:MarkToAll("Waypoint P0 " ..self.tanker:GetName()) p1:MarkToAll("Racetrack P1 "..self.tanker:GetName()) p2:MarkToAll("Racetrack P2 "..self.tanker:GetName()) - self.tanker:SmokeRed() end -- Waypoints array. @@ -713,15 +834,15 @@ function RECOVERYTANKER:onafterRTB(From, Event, To, airbase) airbase=airbase or self.airbase -- Debug message. - local text=string.format("Tanker %s returning to airbase %s.", self.tanker:GetName(), airbase:GetName()) - self:I(text) + local text=string.format("Recoery tanker %s returning to airbase %s.", self.tanker:GetName(), airbase:GetName()) + self:T(text) -- Waypoint array. local wp={} -- Set landing waypoint. wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil, 300, {}, "Current Position") - wp[2]=airbase:GetCoordinate():WaypointAirLanding(300, airbase, nil, "Land at airbase") + wp[2]=airbase:GetCoordinate():SetAltitude(500):WaypointAirLanding(300, airbase, nil, "Land at airbase") -- Initialize WP and route tanker. self.tanker:WayPointInitialize(wp) @@ -763,7 +884,7 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) if groupname:match(self.tankergroupname) then -- Debug info. - self:I(string.format("Respawning recovery tanker group %s.", group:GetName())) + self:T(string.format("Respawning recovery tanker group %s.", group:GetName())) -- Respawn tanker. self.tanker=group:RespawnAtCurrentAirbase() @@ -799,10 +920,10 @@ function RECOVERYTANKER:_RefuelingStart(EventData) end -- Info message. - self:I(string.format("Recovery tanker %s started refueling unit %s", self.tanker:GetName(), unit:GetName())) + self:T(string.format("Recovery tanker %s started refueling unit %s", self.tanker:GetName(), unit:GetName())) -- FMS state "Refueling". - self:Refuel(unit) + self:RefuelStart(receiver) end @@ -827,10 +948,10 @@ function RECOVERYTANKER:_RefuelingStop(EventData) end -- Info message. - self:I(string.format("Recovery tanker %s stopped refueling unit %s", self.tanker:GetName(), unit:GetName())) + self:T(string.format("Recovery tanker %s stopped refueling unit %s", self.tanker:GetName(), unit:GetName())) -- FSM state "Running". - self:Run() + self:RefuelStop(unit) end end @@ -871,7 +992,7 @@ function RECOVERYTANKER:_InitRoute(dist, delay) delay=delay or 1 -- Debug message. - self:I(string.format("Initializing route for recovery tanker %s.", self.tanker:GetName())) + self:T(string.format("Initializing route for recovery tanker %s.", self.tanker:GetName())) -- Carrier position. local Carrier=self.carrier:GetCoordinate() @@ -902,6 +1023,9 @@ function RECOVERYTANKER:_InitRoute(dist, delay) -- Set route. self.tanker:Route(wp, delay) + -- Set state to Running. Necessary when tanker was RTB and respawned since it is probably in state "Returning". + self:__Run(1) + -- No update yet, wait until the function is called (avoids checks if pattern update is needed). self.Tupdate=nil end @@ -920,7 +1044,7 @@ function RECOVERYTANKER:_CheckPatternUpdate(dt) local vC=self.carrier:GetOrientationX() -- Check if tanker is running and last updated is more than 10 minutes ago. - if self:IsRunning() and dt>10*60 then + if self:IsRunning() and dt>self.dTupdate then -- Last saved orientation of carrier. local vP=self.orientation @@ -932,19 +1056,17 @@ function RECOVERYTANKER:_CheckPatternUpdate(dt) local rhdg=math.deg(math.acos(UTILS.VecDot(vC,vP)/UTILS.VecNorm(vC)/UTILS.VecNorm(vP))) -- Check if orientation changed. - -- TODO: make 5 deg input variable. - if math.abs(rhdg)>5 then - self:I(string.format("Carrier heading changed by %d degrees. Updating recovery tanker pattern.", rhdg)) + if math.abs(rhdg)>self.Hupdate then + self:T(string.format("Carrier heading changed by %d degrees. Updating recovery tanker pattern.", rhdg)) update=true end -- Get distance to saved position. local dist=pos:Get2DDistance(self.position) - -- Check if carrier moved more than 10 km. - -- TODO: make 10 km input variable. - if dist/1000>10 then - self:I(string.format("Carrier position changed by %.1f km. Updating recovery tanker pattern.", dist/1000)) + -- Check if carrier moved more than ~10 km. + if dist>self.Dupdate then + self:T(string.format("Carrier position changed by %.1f km. Updating recovery tanker pattern.", dist/1000)) update=true end @@ -979,7 +1101,7 @@ function RECOVERYTANKER:_ActivateTACAN(delay) if unit:IsAlive() then -- Debug message. - self:I(string.format("Activating recovery tanker TACAN beacon: channel=%d mode=%s, morse=%s.", self.TACANchannel, self.TACANmode, self.TACANmorse)) + self:T(string.format("Activating recovery tanker TACAN beacon: channel=%d mode=%s, morse=%s.", self.TACANchannel, self.TACANmode, self.TACANmorse)) -- Create a new beacon and activate TACAN. self.beacon=BEACON:New(unit) diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 691cf583b..6c2b6989c 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -84,7 +84,7 @@ -- -- The implementation allows to customize quite a few settings easily -- --- ## Adjusting the Takeoff Type +-- ## Takeoff Type -- -- By default, the helo is spawned with running engies on the carrier. The mission designer has set option to set the take off type via the @{#RESCUEHELO.SetTakeoff} function. -- Or via shortcuts @@ -108,7 +108,7 @@ -- -- If the helo should no be respawned at all, one can set @{#RESCUEHELO.SetRespawnOff}(). -- --- ## Setting a Home Base +-- ## Home Base -- -- It is possible to define a "home base" other than the aircaft carrier. For example, one could imagine a strike group, and the helo will be spawned from -- another ship which has a helo pad. @@ -123,7 +123,7 @@ -- Once the helo runs out of fuel, it will return to the USS Normandy and not the Stennis for respawning. -- -- --- # Adjusting the Formation Positon +-- ## Formation Positon -- -- The position of the helo relative to the mother ship can be tuned via the functions -- @@ -162,14 +162,14 @@ RESCUEHELO = { --- Class version. -- @field #string version -RESCUEHELO.version="0.9.4" +RESCUEHELO.version="0.9.4w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Add option to stop carrier while rescue operation is in progress? -- TODO: Write documenation. +-- TODO: Add option to stop carrier while rescue operation is in progress? Done but NOT working! -- DONE: Add option to deactivate the rescueing. -- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. -- DONE: Add rescue event when aircraft crashes. From e16f306e1d8a210e55784e2e89c5e488cb44568b Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 27 Nov 2018 00:03:45 +0100 Subject: [PATCH 061/147] AIRBOSS v0.3.6 --- Moose Development/Moose/Ops/Airboss.lua | 334 +++++++++++++------- Moose Development/Moose/Utilities/Utils.lua | 7 +- 2 files changed, 223 insertions(+), 118 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index f947fb967..dfbb9bb0d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -8,7 +8,6 @@ -- * Supports human pilots as well as AI flight groups. -- * Automatic LSO grading. -- * Different skill levels from tipps on-the-fly for students to complete ziplip for pros. --- * Rescue helo option. -- * Recovery tanker option. -- * Voice overs for LSO and AIRBOSS calls. Can easily be customized by users. -- * Automatic TACAN and ICLS channel setting. @@ -70,7 +69,7 @@ -- @field #table flights List of all flights in the CCA. -- @field #table Qmarshal Queue of marshalling aircraft groups. -- @field #table Qpattern Queue of aircraft groups in the landing pattern. --- @field Ops.RescueHelo#RESCUEHELO rescuehelo Rescue helo flying in close formation with the carrier. +-- @field #number Nmaxpattern Max number of aircraft in landing pattern. -- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. -- @field Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. -- @field #table recoverytime List of time intervals when aircraft are recovered. @@ -142,11 +141,10 @@ AIRBOSS = { Qpattern = {}, Qmarshal = {}, Nmaxpattern = nil, - rescuehelo = nil, tanker = nil, warehouse = nil, recoverytime = {}, - holdingoffset= 0, + holdingoffset= nil, } --- Player aircraft types capable of landing on carriers. @@ -456,7 +454,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.5w" +AIRBOSS.version="0.3.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -466,17 +464,17 @@ AIRBOSS.version="0.3.5w" -- TODO: Add radio transmission queue for LSO and airboss. -- TODO: Get correct wire when trapped. -- TODO: Add radio check (LSO, AIRBOSS) to F10 radio menu. --- TODO: Right pattern step after bolter/wo/patternWO? --- TODO: Handle crash event. Delete A/C from queue, send rescue helo, stop carrier? --- TODO: Get fuel state in pounds. -- TODO: Add user functions. -- TODO: Generalize parameters for other carriers. -- TODO: Generalize parameters for other aircraft. --- TODO: CASE II. --- TODO: CASE III. -- TODO: Foul deck check. -- TODO: Persistence of results. -- TODO: Strike group with helo bringing cargo etc. +-- TODO: Right pattern step after bolter/wo/patternWO? +-- TODO: CASE II. +-- TODO: CASE III. +-- DONE: Handle crash event. Delete A/C from queue, send rescue helo. +-- DONE: Get fuel state in pounds. (working for the hornet, did not check others) -- DONE: Add aircraft numbers in queue to carrier info F10 radio output. -- DONE: Monitor holding of players/AI in zoneHolding. -- DONE: Transmission via radio. @@ -530,7 +528,9 @@ function AIRBOSS:New(carriername, alias) -- Create carrier beacon. self.beacon=BEACON:New(self.carrier) - + + -- Defaults: + -- Set up Airboss radio. self.Carrierradio=RADIO:New(self.carrier) self.Carrierradio:SetAlias("AIRBOSS") @@ -541,6 +541,28 @@ function AIRBOSS:New(carriername, alias) self.LSOradio:SetAlias("LSO") self:SetLSOradio() + -- Set ICSL to channel 1. + self:SetICLS() + + -- Set TACAN to channel 74X + self:SetTACAN() + + -- Set max aircraft in landing pattern. + self:SetMaxLandingPattern(1) + + -- Set holding offset to 0 degrees. + self:SetHoldingOffsetAngle(30) + + -- Default recovery case. + self:SetRecoveryCase(1) + + -- CCA 50 NM radius zone around the carrier. + self:SetCarrierControlledArea() + + -- CCZ 5 NM radius zone around the carrier. + self:SetCarrierControlledZone() + + -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then self:_InitStennis() @@ -562,10 +584,13 @@ function AIRBOSS:New(carriername, alias) self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, 0.5*1000, {dx=-UTILS.NMToMeters(3), dy=100, relative_to_unit=true}) -- CASE II/III moving zones. - local angle=180+self.carrierparam.rwyangle - self.zonePlatform = ZONE_UNIT:New("Platform Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters(20), theta=angle, relative_to_unit=true}) - self.zoneDirtyup = ZONE_UNIT:New("Dirty Up Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters(10), theta=angle, relative_to_unit=true}) - self.zoneBullseye = ZONE_UNIT:New("Bulleye Zone", self.carrier, 1.5*1000, {rho=UTILS.NMToMeters( 3), theta=angle, relative_to_unit=true}) + local radial=180+self.carrierparam.rwyangle + local radius=UTILS.NMToMeters(1) + self.zonePlatform = ZONE_UNIT:New("Platform Zone", self.carrier, radius, {rho=UTILS.NMToMeters(19), theta=radial+self.holdingoffset, relative_to_unit=true}) + self.zoneArcturn1 = ZONE_UNIT:New("ArcTurn1 Zone", self.carrier, radius, {rho=UTILS.NMToMeters(14), theta=radial+self.holdingoffset, relative_to_unit=true}) + self.zoneArcturn2 = ZONE_UNIT:New("ArcTurn2 Zone", self.carrier, radius, {rho=UTILS.NMToMeters(12), theta=radial, relative_to_unit=true}) + self.zoneDirtyup = ZONE_UNIT:New("DirtyUp Zone", self.carrier, radius, {rho=UTILS.NMToMeters( 9), theta=radial, relative_to_unit=true}) + self.zoneBullseye = ZONE_UNIT:New("Bulleye Zone", self.carrier, radius, {rho=UTILS.NMToMeters( 3), theta=radial, relative_to_unit=true}) -- Smoke zones. if self.Debug then @@ -576,15 +601,6 @@ function AIRBOSS:New(carriername, alias) --local zp=self:_GetCase23ValidZone():SmokeZone(SMOKECOLOR.Green, 45) end - -- CCA 50 NM radius zone around the carrier. - self:SetCarrierControlledArea() - - -- CCZ 5 NM radius zone around the carrier. - self:SetCarrierControlledZone() - - -- Default recovery case. - self:SetRecoveryCase(1) - -- Init default sound files. for _name,_sound in pairs(AIRBOSS.Soundfile) do local sound=_sound --#AIRBOSS.RadioSound @@ -698,6 +714,18 @@ function AIRBOSS:SetRecoveryCase(case) return self end +--- Set holding pattern offset from final bearing for Case II/III recoveries. +-- Usually, this is +-15 or +-30 degrees. +-- @param #AIRBOSS self +-- @param #number offset Offset angle in degrees. Default 0. +-- @return #AIRBOSS self +function AIRBOSS:SetHoldingOffsetAngle(offset) + + self.holdingoffset=offset or 0 + + return self +end + --- Add recovery time slot. -- @param #AIRBOSS self -- @param #string starttime Start time, e.g. "8:00" for eight o'clock. @@ -787,12 +815,12 @@ function AIRBOSS:SetCarrierradio(frequency, modulation) end ---- Define rescue helicopter associated with the carrier. +--- Set number of aircraft units which can be in the landing pattern before the pattern is full. -- @param #AIRBOSS self --- @param Ops.RescueHelo#RESCUEHELO rescuehelo Rescue helo object. +-- @param #number nmax Max number. Default 4. -- @return #ARIBOSS self -function AIRBOSS:SetRescueHelo(rescuehelo) - self.rescuehelo=rescuehelo +function AIRBOSS:SetMaxLandingPattern(nmax) + self.Nmaxpattern=nmax or 4 return self end @@ -862,12 +890,12 @@ function AIRBOSS:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Land) self:HandleEvent(EVENTS.Crash) - --self:HandleEvent(EVENTS.Ejection) + self:HandleEvent(EVENTS.Ejection) -- Time stamp for checking queues. self.Tqueue=timer.getTime() - -- Init status check + -- Start status check in 1 second. self:__Status(1) end @@ -994,7 +1022,7 @@ function AIRBOSS:onbeforeRecover(From, Event, To) end ---- On after Stop event. Unhandle events and stop status updates. +--- On after Stop event. Unhandle events. -- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. @@ -1003,6 +1031,7 @@ function AIRBOSS:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Birth) self:UnHandleEvent(EVENTS.Land) self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Ejection) end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1178,10 +1207,14 @@ end -- @return #number Speed in m/s or nil. function AIRBOSS:_GetAircraftParameters(playerData, step) + -- Get parameters depended on step. step=step or playerData.step + + -- Get AC type. local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC + -- Return values. local alt local aoa local dist @@ -1271,7 +1304,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) alt=UTILS.FeetToMeters(500) end - aoa=8.1 + aoa=8.1 elseif step==AIRBOSS.PatternStep.WAKE then @@ -1318,7 +1351,7 @@ function AIRBOSS:_CheckQueue() local nmarshal,_=self:_GetQueueInfo(self.Qmarshal) -- Check if there are flights in marshal strack and if the pattern is free. - if nmarshal>0 and npattern<1 then + if nmarshal>0 and npattern stay in pattern and try again. - playerData.step=AIRBOSS.PatternStep.COMMENCING - elseif playerData.patternwo then - -- CASE I pattern wave off. - -- Ask again? Back to marshal. - playerData.step=AIRBOSS.PatternStep.COMMENCING - end - - elseif flight.case==2 then - - - - elseif flight.case==3 then - - end - - else - end - ]] - - playerData.step=AIRBOSS.PatternStep.COMMENCING - - + elseif playerData.landed and not playerData.unit:InAir() then -- Remove player unit from flight and all queues. self:_RemoveUnitFromFlight(playerData.unit) -- Message to player. - self:MessageToPlayer(playerData, "Welcome to the carrier!", "LSO", nil, 10) + self:MessageToPlayer(playerData, "Welcome on board!", "LSO", nil, 10) else @@ -4721,6 +4751,56 @@ function AIRBOSS:_IsHuman(group) return false end +--- Get fuel state in pounds. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. +-- @return #number Fuel state in pounds. +function AIRBOSS:_GetFuelState(unit) + + -- Get relative fuel [0,1]. + local fuel=unit:GetFuel() + + -- Get max weight of fuel in kg. + local maxfuel=self:_GetUnitMasses(unit) + + -- Fuel state, i.e. what let's + local fuelstate=fuel*maxfuel + + -- Debug info. + self:I(self.lid..string.format("Unit %s fuel state = %.1f kg = %.1f lbs", unit:GetName(), fuelstate, UTILS.kg2lbs(fuelstate))) + + return UTILS.kg2lbs(fuelstate) +end + +--- Get unit masses especially fuel from DCS descriptor values. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. +-- @return #number Mass of fuel in kg. +-- @return #number Empty weight of unit in kg. +-- @return #number Max weight of unit in kg. +-- @return #number Max cargo weight in kg. +function AIRBOSS:_GetUnitMasses(unit) + + -- Get DCS descriptors table. + local Desc=unit:GetDesc() + + -- Mass of fuel in kg. + local massfuel=Desc.fuelMassMax or 0 + + -- Mass of empty unit in km. + local massempty=Desc.massEmpty or 0 + + -- Max weight of unit in kg. + local massmax=Desc.massMax or 0 + + -- Rest is cargo. + local masscargo=massmax-massfuel-massempty + + -- Debug info. + self:I(self.lid..string.format("Unit %s mass fuel=%.1f kg, empty=%.1f kg, max=%.1f kg, cargo=%.1f kg", unit:GetName(), massfuel, massempty, massmax, masscargo)) + + return massfuel, massempty, massmax, masscargo +end --- Get player data from unit object -- @param #AIRBOSS self @@ -5015,6 +5095,7 @@ function AIRBOSS:_RequestCommence(_unitName) -- Get stack value. local stack=playerData.flag:Get() + -- Check if player is in the lowest stack. if stack>1 then -- We are in a higher stack. text="Negative ghostrider, it's not your turn yet!" @@ -5023,8 +5104,8 @@ function AIRBOSS:_RequestCommence(_unitName) -- Number of aircraft currently in pattern. local _,npattern=self:_GetQueueInfo(self.Qpattern) - -- TODO: set nmax for pattern. Should be ~6 but let's make this 4. - if npattern>0 then + -- Check if pattern is already full. + if npattern>self.Nmaxpattern then -- Patern is full! text=string.format("Negative ghostrider, pattern is full! There are %d aircraft currently in pattern.", npattern) else @@ -5493,6 +5574,14 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then + + -- Stack and stack altitude. + local stack=playerData.flag:Get() + local stackalt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) + + -- Fuel and fuel state. + local fuel=playerData.unit:GetFuel()*100 + local fuelstate=self:_GetFuelState(playerData.unit) -- Player data. local text=string.format("Status of player %s (%s)\n", playerData.name, playerData.callsign) @@ -5501,15 +5590,11 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) text=text..string.format("Skil level: %s\n", playerData.difficulty) text=text..string.format("Aircraft: %s\n", playerData.actype) text=text..string.format("Board number: %s\n", playerData.onboard) - text=text..string.format("Fuel: %.1f %%\n", playerData.unit:GetFuel()*100) - local stack=playerData.flag:Get() - local stackalt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) - text=text..string.format("Flag/stack: %d\n", stack) - text=text..string.format("Stack alt: %d ft\n", stackalt) + text=text..string.format("Fuel state: %.1f lbs/1000 (%.1f %%)\n", fuelstate/1000, fuel) + text=text..string.format("Stack: %d alt=%d ft\n", stack, stackalt) text=text..string.format("Group: %s\n", playerData.group:GetName()) - text=text..string.format("# units: %d\n", #playerData.group:GetUnits()) - text=text..string.format("n units: %d\n", playerData.nunits) - text=text..string.format("Section Lead: %s\n", tostring(playerData.seclead)) + text=text..string.format("# units: %d (n=%d)\n", #playerData.group:GetUnits(), playerData.nunits) + text=text..string.format("Section Lead: %s\n", tostring(playerData.seclead)) text=text..string.format("# section: %d", #playerData.section) for _,_sec in pairs(playerData.section) do local sec=_sec --#AIRBOSS.PlayerData @@ -5601,31 +5686,46 @@ function AIRBOSS:_MarkCase23Zones(_unitName, flare) if playerData then - local text="CASE II/III: Marking:\n" + -- Initial + local text="Marking CASE II/III zone:\n" --TODO: Add height! if flare then - text=text.."* Valid zone with GREEN flares\n" + text=text.."* valid area with GREEN flares\n" local zp=self:_GetCase23ValidZone() zp:FlareZone(FLARECOLOR.Green, 45) - text=text.."* Platform zone with RED flares\n" + text=text.."* platform with RED flares\n" self.zonePlatform:FlareZone(FLARECOLOR.Red, 45) - text=text.."* Dirty up zone with YELLOW flares\n" + text=text.."* dirty up with YELLOW flares\n" self.zoneDirtyup:FlareZone(FLARECOLOR.Yellow, 45) - text=text.."* Bullseye zone with WHITE flares\n" + if math.abs(self.holdingoffset)>0 then + self.zoneArcturn1:FlareZone(FLARECOLOR.Yellow, 45) + text=text.."* arc turn in with YELLOW flares\n" + self.zoneArcturn2:FlareZone(FLARECOLOR.White, 45) + text=text.."* arc trun out with WHITE flares\n" + end + text=text.."* bullseye with WHITE flares\n" self.zoneBullseye:FlareZone(FLARECOLOR.White, 45) else - text=text.."* Valid zone with GREEN smoke\n" + text=text.."* valid area with GREEN smoke\n" local zp=self:_GetCase23ValidZone() zp:SmokeZone(SMOKECOLOR.Green, 45) - text=text.."* Platform zone with RED smoke\n" + text=text.."* platform with RED smoke\n" self.zonePlatform:SmokeZone(SMOKECOLOR.Red, 45) - text=text.."* Dirty up zone with ORANGE flares\n" + text=text.."* dirty up with ORANGE flares\n" + if math.abs(self.holdingoffset)>0 then + self.zoneArcturn1:SmokeZone(SMOKECOLOR.Red, 45) + text=text.."* arc turn in with YELLOW flares\n" + self.zoneArcturn2:SmokeZone(SMOKECOLOR.Orange, 45) + text=text.."* arc trun out with WHITE flares\n" + end + self.zoneDirtyup:SmokeZone(SMOKECOLOR.Orange, 45) - text=text.."* Bullseye zone with BLUE smoke\n" + text=text.."* bullseye with BLUE smoke\n" self.zoneBullseye:SmokeZone(SMOKECOLOR.Blue, 45) end + -- Send message to player. self:MessageToPlayer(playerData, text, "AIRBOSS", "", 10) end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index be2754d19..9eee42af4 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -308,7 +308,12 @@ UTILS.hPa2mmHg = function( hPa ) return hPa * 0.7500615613030 end - +--- Convert kilo gramms (kg) to pounds (lbs). +-- @param #number kg Mass in kg. +-- @return #number Mass in lbs. +UTILS.kg2lbs = function( kg ) + return kg * 2.20462 +end --[[acc: in DM: decimal point of minutes. From bd537ece0042f46496b13bc36c04f2b7e4953ae4 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 27 Nov 2018 18:38:21 +0100 Subject: [PATCH 062/147] Revert "Merge branch 'develop' into FF/Develop" This reverts commit 1f282693b39a67de508d959bf46279b96965d331, reversing changes made to e16f306e1d8a210e55784e2e89c5e488cb44568b. --- Moose Development/Moose/AI/AI_A2A.lua | 23 +- .../Moose/AI/AI_A2A_Dispatcher.lua | 238 +- Moose Development/Moose/AI/AI_A2G.lua | 69 - .../Moose/AI/AI_A2G_Dispatcher.lua | 4146 ----------------- Moose Development/Moose/AI/AI_A2G_Engage.lua | 440 -- Moose Development/Moose/AI/AI_A2G_Patrol.lua | 488 -- Moose Development/Moose/AI/AI_Air.lua | 732 --- Moose Development/Moose/AI/AI_Formation.lua | 249 +- Moose Development/Moose/Core/Point.lua | 429 +- Moose Development/Moose/Core/Set.lua | 4 +- Moose Development/Moose/Core/Spawn.lua | 6 +- Moose Development/Moose/Core/Zone.lua | 28 +- .../Moose/Functional/Artillery.lua | 180 +- .../Moose/Functional/Designate.lua | 26 +- Moose Development/Moose/Functional/RAT.lua | 23 +- Moose Development/Moose/Functional/Range.lua | 870 ++-- .../Moose/Functional/Warehouse.lua | 102 +- .../Moose/Functional/ZoneCaptureCoalition.lua | 19 - .../Moose/Wrapper/Controllable.lua | 47 +- Moose Development/Moose/Wrapper/Group.lua | 2 +- Moose Development/Moose/Wrapper/Static.lua | 33 - Moose Development/Moose/Wrapper/Unit.lua | 22 +- Moose Setup/Moose.files | 5 - 23 files changed, 1080 insertions(+), 7101 deletions(-) delete mode 100644 Moose Development/Moose/AI/AI_A2G.lua delete mode 100644 Moose Development/Moose/AI/AI_A2G_Dispatcher.lua delete mode 100644 Moose Development/Moose/AI/AI_A2G_Engage.lua delete mode 100644 Moose Development/Moose/AI/AI_A2G_Patrol.lua delete mode 100644 Moose Development/Moose/AI/AI_Air.lua diff --git a/Moose Development/Moose/AI/AI_A2A.lua b/Moose Development/Moose/AI/AI_A2A.lua index c96fa4e43..33ee16ace 100644 --- a/Moose Development/Moose/AI/AI_A2A.lua +++ b/Moose Development/Moose/AI/AI_A2A.lua @@ -438,14 +438,13 @@ function AI_A2A:onafterStatus() RTB = false end end - --- I think this code is not requirement anymore after release 2.5. --- if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then --- if DistanceFromHomeBase < 5000 then --- self:E( self.Controllable:GetName() .. " is near the home base, RTB!" ) --- self:Home( "Destroy" ) --- end --- end + + if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then + if DistanceFromHomeBase < 5000 then + self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) + self:Home( "Destroy" ) + end + end if not self:Is( "Fuel" ) and not self:Is( "Home" ) then @@ -482,12 +481,9 @@ function AI_A2A:onafterStatus() end -- Check if planes went RTB and are out of control. - -- We only check if planes are out of control, when they are in duty. if self.Controllable:HasTask() == false then if not self:Is( "Started" ) and not self:Is( "Stopped" ) and - not self:Is( "Fuel" ) and - not self:Is( "Damaged" ) and not self:Is( "Home" ) then if self.IdleCount >= 2 then if Damage ~= InitialLife then @@ -507,11 +503,8 @@ function AI_A2A:onafterStatus() if RTB == true then self:__RTB( 0.5 ) end - - if not self:Is("Home") then - self:__Status( 10 ) - end + self:__Status( 10 ) end end diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 9b9370bb3..8ba529d19 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -274,7 +274,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher_Red = AI_A2A_DISPATCHER:New( EWR_Red ) -- A2ADispatcher_Blue = AI_A2A_DISPATCHER:New( EWR_Blue ) -- - -- ### 1.2. Define the detected **target grouping radius**: + -- ### 2. Define the detected **target grouping radius**: -- -- The target grouping radius is a property of the Detection object, that was passed to the AI\_A2A\_DISPATCHER object, but can be changed. -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. @@ -1013,48 +1013,12 @@ do -- AI_A2A_DISPATCHER self:SetTacticalDisplay( false ) - self.DefenderCAPIndex = 0 - self:__Start( 5 ) return self end - --- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:onafterStart( From, Event, To ) - - self:GetParent( self, AI_A2A_DISPATCHER ).onafterStart( self, From, Event, To ) - - -- Spawn the resources. - for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do - DefenderSquadron.Resource = {} - if DefenderSquadron.ResourceCount then - for Resource = 1, DefenderSquadron.ResourceCount do - self:ParkDefender( DefenderSquadron ) - end - end - end - end - - - --- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:ParkDefender( DefenderSquadron ) - local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) - local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN - Spawn:InitGrouping( 1 ) - local SpawnGroup - if self:IsSquadronVisible( DefenderSquadron.Name ) then - SpawnGroup = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, SPAWN.Takeoff.Cold ) - local GroupName = SpawnGroup:GetName() - DefenderSquadron.Resources = DefenderSquadron.Resources or {} - DefenderSquadron.Resources[TemplateID] = DefenderSquadron.Resources[TemplateID] or {} - DefenderSquadron.Resources[TemplateID][GroupName] = {} - DefenderSquadron.Resources[TemplateID][GroupName] = SpawnGroup - end - end - - --- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2A_DISPATCHER:OnEventBaseCaptured( EventData ) @@ -1066,7 +1030,7 @@ do -- AI_A2A_DISPATCHER -- Now search for all squadrons located at the airbase, and sanatize them. for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do if Squadron.AirbaseName == AirbaseName then - Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. + Squadron.Resources = -999 -- The base has been captured, and the resources are eliminated. No more spawning. Squadron.Captured = true self:I( "Squadron " .. SquadronName .. " captured." ) end @@ -1095,7 +1059,6 @@ do -- AI_A2A_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() - self:ParkDefender( Squadron, Defender ) return end if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then @@ -1122,7 +1085,6 @@ do -- AI_A2A_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() - self:ParkDefender( Squadron, Defender ) end end end @@ -1512,7 +1474,7 @@ do -- AI_A2A_DISPATCHER -- Just remember that your template (groups late activated) need to start with the prefix you have specified in your code. -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets. -- - -- @param #number ResourceCount (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. + -- @param #number Resources (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. -- -- @usage -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. @@ -1535,13 +1497,13 @@ do -- AI_A2A_DISPATCHER -- -- @usage -- -- This is an example like the previous, but now with infinite resources. - -- -- The ResourceCount parameter is not given in the SetSquadron method. + -- -- The Resources parameter is not given in the SetSquadron method. -- A2ADispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) -- A2ADispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) -- -- -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) + function AI_A2A_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, Resources ) self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} @@ -1566,11 +1528,11 @@ do -- AI_A2A_DISPATCHER DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate] end end - DefenderSquadron.ResourceCount = ResourceCount + DefenderSquadron.Resources = Resources DefenderSquadron.TemplatePrefixes = TemplatePrefixes DefenderSquadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured. - self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) + self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, Resources } } ) return self end @@ -1589,54 +1551,6 @@ do -- AI_A2A_DISPATCHER end - --- Set the Squadron visible before startup of the dispatcher. - -- All planes will be spawned as uncontrolled on the parking spot. - -- They will lock the parking spot. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @return #AI_A2A_DISPATCHER - -- @usage - -- - -- -- Set the Squadron visible before startup of dispatcher. - -- A2ADispatcher:SetSquadronVisible( "Mineralnye" ) - -- - function AI_A2A_DISPATCHER:SetSquadronVisible( SquadronName ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.Uncontrolled = true - - for SpawnTemplate, DefenderSpawn in pairs( self.DefenderSpawns ) do - DefenderSpawn:InitUnControlled() - end - - end - - --- Check if the Squadron is visible before startup of the dispatcher. - -- @param #AI_A2A_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @return #bool true if visible. - -- @usage - -- - -- -- Set the Squadron visible before startup of dispatcher. - -- local IsVisible = A2ADispatcher:IsSquadronVisible( "Mineralnye" ) - -- - function AI_A2A_DISPATCHER:IsSquadronVisible( SquadronName ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - if DefenderSquadron then - return DefenderSquadron.Uncontrolled == true - end - - return nil - - end - --- Set a CAP for a Squadron. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -1785,7 +1699,7 @@ do -- AI_A2A_DISPATCHER if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured. - if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + if ( not DefenderSquadron.Resources ) or ( DefenderSquadron.Resources and DefenderSquadron.Resources > 0 ) then -- And, if there are sufficient resources. local Cap = DefenderSquadron.Cap if Cap then @@ -1818,7 +1732,7 @@ do -- AI_A2A_DISPATCHER if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured. - if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + if ( not DefenderSquadron.Resources ) or ( DefenderSquadron.Resources and DefenderSquadron.Resources > 0 ) then -- And, if there are sufficient resources. local Gci = DefenderSquadron.Gci if Gci then return DefenderSquadron @@ -2576,21 +2490,21 @@ do -- AI_A2A_DISPATCHER self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() self.Defenders[ DefenderName ] = Squadron - if Squadron.ResourceCount then - Squadron.ResourceCount = Squadron.ResourceCount - Size + if Squadron.Resources then + Squadron.Resources = Squadron.Resources - Size end - self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + self:F( { DefenderName = DefenderName, SquadronResources = Squadron.Resources } ) end --- @param #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() - if Squadron.ResourceCount then - Squadron.ResourceCount = Squadron.ResourceCount + Defender:GetSize() + if Squadron.Resources then + Squadron.Resources = Squadron.Resources + Defender:GetSize() end self.Defenders[ DefenderName ] = nil - self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + self:F( { DefenderName = DefenderName, SquadronResources = Squadron.Resources } ) end function AI_A2A_DISPATCHER:GetSquadronFromDefender( Defender ) @@ -2732,80 +2646,7 @@ do -- AI_A2A_DISPATCHER return Friendlies end - - --- - -- @param #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded ) - local SquadronName = DefenderSquadron.Name - DefendersNeeded = DefendersNeeded or 4 - local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping - DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded - - if self:IsSquadronVisible( SquadronName ) then - - -- Here we CAP the new planes. - -- The Resources table is filled in advance. - local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template. - - -- We determine the grouping based on the parameters set. - self:F( { DefenderGrouping = DefenderGrouping } ) - - -- New we will form the group to spawn in. - -- We search for the first free resource matching the template. - local DefenderUnitIndex = 1 - local DefenderCAPTemplate = nil - local DefenderName = nil - for GroupName, DefenderGroup in pairs( DefenderSquadron.Resources[TemplateID] or {} ) do - self:F( { GroupName = GroupName } ) - local DefenderTemplate = _DATABASE:GetGroupTemplate( GroupName ) - if DefenderUnitIndex == 1 then - DefenderCAPTemplate = UTILS.DeepCopy( DefenderTemplate ) - self.DefenderCAPIndex = self.DefenderCAPIndex + 1 - DefenderCAPTemplate.name = SquadronName .. "#" .. self.DefenderCAPIndex .. "#" .. GroupName - DefenderName = DefenderCAPTemplate.name - else - -- Add the unit in the template to the DefenderCAPTemplate. - local DefenderUnitTemplate = DefenderTemplate.units[1] - DefenderCAPTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate - end - DefenderUnitIndex = DefenderUnitIndex + 1 - DefenderSquadron.Resources[TemplateID][GroupName] = nil - if DefenderUnitIndex > DefenderGrouping then - break - end - - end - - if DefenderCAPTemplate then - local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) - local SpawnGroup = GROUP:Register( DefenderName ) - DefenderCAPTemplate.lateActivation = nil - DefenderCAPTemplate.uncontrolled = nil - local Takeoff = self:GetSquadronTakeoff( SquadronName ) - DefenderCAPTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type - DefenderCAPTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action - local Defender = _DATABASE:Spawn( DefenderCAPTemplate ) - - self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) - return Defender, DefenderGrouping - end - else - local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN - if DefenderGrouping then - Spawn:InitGrouping( DefenderGrouping ) - else - Spawn:InitGrouping() - end - - local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) - local Defender = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP - self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) - return Defender, DefenderGrouping - end - - return nil, nil - end --- -- @param #AI_A2A_DISPATCHER self @@ -2822,9 +2663,15 @@ do -- AI_A2A_DISPATCHER local Cap = DefenderSquadron.Cap if Cap then + + local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + Spawn:InitGrouping( DefenderGrouping ) - local DefenderCAP, DefenderGrouping = self:ResourceActivate( DefenderSquadron ) - + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local DefenderCAP = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) + self:AddDefenderToSquadron( DefenderSquadron, DefenderCAP, DefenderGrouping ) + if DefenderCAP then local Fsm = AI_A2A_CAP:New( DefenderCAP, Cap.Zone, Cap.FloorAltitude, Cap.CeilingAltitude, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.AltType ) @@ -2839,7 +2686,7 @@ do -- AI_A2A_DISPATCHER self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", Fsm ) function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"CAP Birth", Defender:GetName()}) + self:F({"GCI Birth", Defender:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER @@ -2873,9 +2720,9 @@ do -- AI_A2A_DISPATCHER if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) Defender:Destroy() - self:ParkDefender( Squadron, Defender ) end end + end end end @@ -2981,19 +2828,31 @@ do -- AI_A2A_DISPATCHER self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } ) self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } ) - -- DefenderSquadron.ResourceCount can have the value nil, which expresses unlimited resources. - -- DefendersNeeded cannot exceed DefenderSquadron.ResourceCount! - if DefenderSquadron.ResourceCount and DefendersNeeded > DefenderSquadron.ResourceCount then - DefendersNeeded = DefenderSquadron.ResourceCount + -- DefenderSquadron.Resources can have the value nil, which expresses unlimited resources. + -- DefendersNeeded cannot exceed DefenderSquadron.Resources! + if DefenderSquadron.Resources and DefendersNeeded > DefenderSquadron.Resources then + DefendersNeeded = DefenderSquadron.Resources BreakLoop = true end while ( DefendersNeeded > 0 ) do - local DefenderGCI, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN + local DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded + if DefenderGrouping then + Spawn:InitGrouping( DefenderGrouping ) + else + Spawn:InitGrouping() + end + + local TakeoffMethod = self:GetSquadronTakeoff( ClosestDefenderSquadronName ) + local DefenderGCI = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP + self:F( { GCIDefender = DefenderGCI:GetName() } ) DefendersNeeded = DefendersNeeded - DefenderGrouping + self:AddDefenderToSquadron( DefenderSquadron, DefenderGCI, DefenderGrouping ) + if DefenderGCI then DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead @@ -3060,7 +2919,6 @@ do -- AI_A2A_DISPATCHER if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) Defender:Destroy() - self:ParkDefender( Squadron, Defender ) end end end -- if DefenderGCI then @@ -3642,7 +3500,7 @@ do -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. - -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. + -- @param #number Resources The amount of resources that will be allocated to each squadron. -- @return #AI_A2A_GCICAP -- @usage -- @@ -3717,7 +3575,7 @@ do -- -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, nil, nil, nil, nil, nil, 30 ) -- - function AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + function AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, Resources ) local EWRSetGroup = SET_GROUP:New() EWRSetGroup:FilterPrefixes( EWRPrefixes ) @@ -3771,7 +3629,7 @@ do end end if Templates then - self:SetSquadron( AirbaseName, AirbaseName, Templates, ResourceCount ) + self:SetSquadron( AirbaseName, AirbaseName, Templates, Resources ) end end @@ -3848,7 +3706,7 @@ do -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. - -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. + -- @param #number Resources The amount of resources that will be allocated to each squadron. -- @return #AI_A2A_GCICAP -- @usage -- @@ -3932,9 +3790,9 @@ do -- -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", nil, nil, nil, nil, nil, 30 ) -- - function AI_A2A_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + function AI_A2A_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, Resources ) - local self = AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + local self = AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, Resources ) if BorderPrefix then self:SetBorderZone( ZONE_POLYGON:New( BorderPrefix, GROUP:FindByName( BorderPrefix ) ) ) diff --git a/Moose Development/Moose/AI/AI_A2G.lua b/Moose Development/Moose/AI/AI_A2G.lua deleted file mode 100644 index 2f6a8f500..000000000 --- a/Moose Development/Moose/AI/AI_A2G.lua +++ /dev/null @@ -1,69 +0,0 @@ ---- **AI** -- Models the process of air to ground operations for airplanes and helicopters. --- --- === --- --- ### Author: **FlightControl** --- --- === --- --- @module AI.AI_A2G --- @image AI_Air_To_Ground_Dispatching.JPG - ---- @type AI_A2G --- @extends AI.AI_Air#AI_AIR - ---- The AI_A2G class implements the core functions to operate an AI @{Wrapper.Group} A2G tasking. --- --- --- # 1) AI_A2G constructor --- --- * @{#AI_A2G.New}(): Creates a new AI_A2G object. --- --- # 2) AI_A2G is a Finite State Machine. --- --- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. --- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. --- --- So, each of the rows have the following structure. --- --- * **From** => **Event** => **To** --- --- Important to know is that an event can only be executed if the **current state** is the **From** state. --- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, --- and the resulting state will be the **To** state. --- --- These are the different possible state transitions of this state machine implementation: --- --- * Idle => Start => Monitoring --- --- ## 2.1) AI_A2G States. --- --- * **Idle**: The process is idle. --- --- ## 2.2) AI_A2G Events. --- --- * **Start**: Start the transport process. --- * **Stop**: Stop the transport process. --- * **Monitor**: Monitor and take action. --- --- @field #AI_A2G -AI_A2G = { - ClassName = "AI_A2G", -} - ---- Creates a new AI_A2G process. --- @param #AI_A2G self --- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process. --- @return #AI_A2G -function AI_A2G:New( AIGroup ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_AIR:New( AIGroup ) ) -- #AI_A2G - - self:SetFuelThreshold( .2, 60 ) - self:SetDamageThreshold( 0.4 ) - self:SetDisengageRadius( 70000 ) - - return self -end - diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua deleted file mode 100644 index fde91d028..000000000 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ /dev/null @@ -1,4146 +0,0 @@ ---- **AI** - Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. --- --- === --- --- Features: --- --- * Setup quickly an A2G defense system for a coalition. --- * Setup multiple defense zones to defend specif points in your battlefield. --- * Setup (SEAD) suppression of air defenses to enhance the control of enemy airspace. --- * Setup (CAS) Controlled Air Support to attack approach enemy ground units. --- * Setup (BAI) Battleground Air Interdiction to attack detected remote enemy ground units and targets. --- * Define and use a detection network setup by recce. --- * Define defense squadrons at airbases, farps and carriers. --- * Enable airbases for A2G defenses. --- * Add different planes and helicopter templates to different squadrons. --- * Assign squadrons to execute a specific engagement type depending on threat level of the detected ground enemy unit composition. --- * Add multiple squadrons to different airbases, farps or carriers. --- * Define different ranges to engage upon. --- * Establish an automatic in air refuel process for planes using refuel tankers. --- * Setup default settings for all squadrons and A2G defenses. --- * Setup specific settings for specific squadrons. --- --- === --- --- ## Missions: --- --- [AID-A2G - AI A2G Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2G%20-%20AI%20A2G%20Dispatching) --- --- === --- --- ## YouTube Channel: --- --- [DCS WORLD - MOOSE - A2G GCICAP - Build an automatic A2G Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) --- --- === --- --- # QUICK START GUIDE --- --- The following class is available to model an A2G defense system. --- --- AI_A2G_DISPATCHER is the main A2G defense class that models the A2G defense system. --- --- Before you start using the AI_A2G_DISPATCHER, ask youself the following questions. --- --- --- ## 1. Which coalition am I modeling an A2G defense system for? blue or red? --- --- One AI_A2G_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. --- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI_A2G_DISPATCHER **objects**, --- each governing their defense system for one coalition. --- --- --- ## 2. Which type of detection will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). --- --- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units and reporting them to the head quarters. --- Several types of @{Functional.Detection} classes exist, and the most common characteristics of these classes is that they: --- --- * Perform detections from multiple recce as one co-operating entity. --- * Communicate with a @{Tasking.CommandCenter}, which consolidates each detection. --- * Groups detections based on a method (per area, per type or per unit). --- * Communicates detections. --- --- --- ## 3. Which recce units can be used as part of the detection system? Only Ground or also Airborne? --- --- Depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets. --- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range. --- Recce are very useful to acquire the position of enemy ground targets when spread out over the battlefield at strategic positions. --- Ground units also have varying detectors, and especially the ground units which have laser guiding missiles can be extremely effective at --- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. --- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then --- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! --- --- Beside ground level units to use for reconnaissance, air units are also very effective. The are capable of patrolling at great speed --- covering a large terrain. However, airborne recce can be vulnerable to air to ground attacks, and you need air superiority to make then --- effective. Also the instruments available at the air units play a big role in the effectiveness of the reconnaissance. --- Air units which have ground detection capabilities will be much more effective than air units with only visual detection capabilities. --- For the red coalition, the Mi-28N and for the blue side, the reaper are such effective reconnaissance airborne units. --- --- --- ## 4. How do defenses decide to engage on approaching enemy units? --- --- The A2G dispacher needs you to setup defense coordinates, which are specific coordinates that are strategic positions in the battle field --- to be defended. Any ground based enemy approaching to such a defense point, will be engaged for defense by A2G defense units. --- The A2G dispatcher provides parameters to setup the defensiveness, meaning, when actually A2G units will engage with the approaching enemy. --- For this, a probability distribution model has been created, which models an increased probability that a defense will engage an attacker, --- depending on the distance of the attacker to the defense coordinate. There are 3 levels of defense reactivity setup, which are Low, Medium and High. --- Defenses will start to consider defensive action when an enemy ground unit is within 60km from a defense point, by default. --- But you can change this maximum distance using on of the available methods. The close the attacker is to the defense point, the --- higher the probability will be that a defense action will be launched! --- --- --- ## 5. Are defense coordinates and defense reactivity the only parameters? --- --- No, depending on the target type, and the threat level of the target, the probability of defense will be higher. --- In other words, when a SAM-10 radar emitter is detected, its probabilty for defense will be much higher than when a BMP-1 vehicle is --- detected, even when both are at the same distance from a defense coordinate. --- This will ensure optimal defenses, SEAD tasks will be much more quicker launched agains radar emitters, to ensure air superiority. --- Approaching main battle tanks will be much faster defended upon, than a group of approaching trucks. --- --- --- ## 6. Which Squadrons will I create and which name will I give each Squadron? --- --- The A2G defense system works with **Squadrons**. Each Squadron must be given a unique name, that forms the **key** to the squadron. --- Several options and activities can be set per Squadron. --- --- There are mainly 3 types of defenses: SEAD, CAS and BAI. --- --- Suppression of Air Defenses (SEAD) are effective agains radar emitters. Close Air Support (CAS) is launched when the enemy is close near friendly units. --- Battleground Air Interdiction (BAI) tasks are launched when there are no friendlies around. --- --- Depending on the defense type, different payloads will be needed. See further points on squadron definition. --- --- ## 7. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? --- --- Squadrons are placed as the "home base" on an airfield, carrier or farp. --- Carefully plan where each Squadron will be located as part of the defense system. --- Any airbase, farp or carrier can act as the launching platform for A2G defenses. --- Carefully plan which airbases will take part in the coalition. Color each airbase in the color of the coalition. --- --- --- ## 8. Which helicopter or plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron? --- --- Per Squadron, one or multiple helicopter or plane models can be allocated as **Templates**. --- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**. --- The A2G defense system will select from the given templates a random template to spawn a new plane (group). --- --- A squadron will perform specific task types (SEAD, CAS or BAI). So, squadrons will require specific templates for the --- task types it will perform. A squadron executing SEAD defenses, will require a payload with long range anti-radar seeking missiles. --- --- --- ## 9. Which payloads, skills and skins will these plane models have? --- --- Per Squadron, even if you have one plane model, you can still allocate multiple templates of one plane model, --- each having different payloads, skills and skins. --- The A2G defense system will select from the given templates a random template to spawn a new plane (group). --- --- --- ## 10. How to squadrons engage in a defensive action? --- --- There are two ways how squadrons engage and execute your A2G defenses. --- Squadrons can start the defense directly from the airbase, farp or carrier. When a squadron launches a defensive group, that group --- will start directly from the airbase. The other way is to launch early on in the mission a patrolling mechanism. --- Squadrons will launch air units to patrol in specific zone(s), so that when ground enemy targets are detected, that the airborne --- A2G defenses can come immediately into action. --- --- --- ## 11. For each Squadron doing a patrol, which zone types will I create? --- --- Per zone, evaluate whether you want: --- --- * simple trigger zones --- * polygon zones --- * moving zones --- --- Depending on the type of zone selected, a different @{Zone} object needs to be created from a ZONE_ class. --- --- --- ## 12. Are moving defense coordinates possible? --- --- Yes, different COORDINATE types are possible to be used. --- The COORDINATE_UNIT will help you to specify a defense coodinate that is attached to a moving unit. --- --- --- ## 13. How much defense coordinates do I need to create? --- --- It depends, but the idea is to define only the necessary defense points that drive your mission. --- If you define too much defense points, the performance of your mission may decrease. Per defense point defined, --- all the possible enemies are evaluated. Note that each defense coordinate has a reach depending on the size of the defense radius. --- The default defense radius is about 60km, and depending on the defense reactivity, defenses will be launched when the enemy is at --- close or greater distance from the defense coordinate. --- --- --- ## 14. For each Squadron doing patrols, what are the time intervals and patrol amounts to be performed? --- --- For each patrol: --- --- * **How many** patrol you want to have airborne at the same time? --- * **How frequent** you want the defense mechanism to check whether to start a new patrol? --- --- other considerations: --- --- * **How far** is the patrol area from the engagement "hot zone". You want to ensure that the enemy is reached on time! --- * **How safe** is the patrol area taking into account air superiority. Is it well defended, are there nearby A2A bases? --- --- --- ## 15. For each Squadron, which takeoff method will I use? --- --- For each Squadron, evaluate which takeoff method will be used: --- --- * Straight from the air --- * From the runway --- * From a parking spot with running engines --- * From a parking spot with cold engines --- --- **The default takeoff method is staight in the air.** --- This takeoff method is the most useful if you want to avoid airplane clutter at airbases! --- But it is the least realistic one! --- --- --- ## 16. For each Squadron, which landing method will I use? --- --- For each Squadron, evaluate which landing method will be used: --- --- * Despawn near the airbase when returning --- * Despawn after landing on the runway --- * Despawn after engine shutdown after landing --- --- **The default landing method is despawn when near the airbase when returning.** --- This landing method is the most useful if you want to avoid airplane clutter at airbases! --- But it is the least realistic one! --- --- --- ## 19. For each Squadron, which **defense overhead** will I use? --- --- For each Squadron, depending on the helicopter or airplane type (modern, old) and payload, which overhead is required to provide any defense? --- --- In other words, if **X** enemy ground units are detected, how many **Y** defense helicpters or airplanes need to engage (per squadron)? --- The **Y** is dependent on the type of airplane (era), payload, fuel levels, skills etc. --- But the most important factor is the payload, which is the amount of A2G weapons the defense can carry to attack the enemy ground units. --- For example, a Ka-50 can carry 16 vikrs, that means, that it potentially can destroy at least 8 ground units without a reload of ammunication. --- That means, that one defender can destroy more enemy ground units. --- Thus, the overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected. --- --- **The default overhead is 1. A smaller value than 1, like 0.25 will decrease the overhead to a 1 / 4 ratio, meaning, --- one defender for each 4 detected ground enemy units. ** --- --- --- ## 19. For each Squadron, which grouping will I use? --- --- When multiple targets are detected, how will defenses be grouped when multiple defense air units are spawned for multiple enemy ground units? --- Per one, two, three, four? --- --- **The default grouping is 1. That means, that each spawned defender will act individually.** --- But you can specify a number between 1 and 4, so that the defenders will act as a group. --- --- === --- --- ### Author: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons). --- --- @module AI.AI_A2G_Dispatcher --- @image AI_Air_To_Ground_Dispatching.JPG - - - -do -- AI_A2G_DISPATCHER - - --- AI_A2G_DISPATCHER class. - -- @type AI_A2G_DISPATCHER - -- @extends Tasking.DetectionManager#DETECTION_MANAGER - - --- Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. - -- - -- === - -- - -- When your mission is in the need to take control of the AI to automate and setup a process of air to ground defenses, this is the module you need. - -- The defense system work through the definition of defense coordinates, which are points in your friendly area within the battle field, that your mission need to have defended. - -- Multiple defense coordinates can be setup. Defense coordinates can be strategic or tactical positions or references to strategic units or scenery. - -- The A2G dispatcher will evaluate every x seconds the tactical situation around each defense coordinate. When a defense coordinate - -- is under threat, it will communicate through the command center that defensive actions need to be taken and will launch groups of air units for defense. - -- The level of threat to the defense coordinate varyies upon the strength and types of the enemy units, the distance to the defense point, and the defensiveness parameters. - -- Defensive actions are taken through probability, but the closer and the more threat the enemy poses to the defense coordinate, the faster it will be attacked by friendly A2G units. - -- - -- Please study carefully the underlying explanations how to setup and use this module, as it has many features. - -- It also requires a little study to ensure that you get a good understanding of the defense mechanisms, to ensure a strong - -- defense for your missions. - -- - -- === - -- - -- # USAGE GUIDE - -- - -- ## 1. AI\_A2G\_DISPATCHER constructor: - -- - -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_DISPATCHER-ME_1.JPG) - -- - -- - -- The @{#AI_A2G_DISPATCHER.New}() method creates a new AI_A2G_DISPATCHER instance. - -- - -- ### 1.1. Define the **reconnaissance network**: - -- - -- As part of the AI_A2G_DISPATCHER :New() constructor, a reconnaissance network must be given as the first parameter. - -- A reconnaissance network is provide through an instance of a @{Functional.Detection} network. - -- The most effective reconnaissance for the A2G dispatcher would be to use the @{Functional.Detection#DETECTION_AREAS} object. - -- - -- An reconnaissance network, is used to detect enemy ground targets, potentially group them into areas, and to understand the position, level of threat of the enemy. - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia5.JPG) - -- - -- As explained in the introduction, depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets. - -- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range. - -- Recce are very useful to acquire the position of enemy ground targets when spread out over the battlefield at strategic positions. - -- Ground units also have varying detectors, and especially the ground units which have laser guiding missiles can be extremely effective at - -- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. - -- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then - -- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! - -- - -- Beside ground level units to use for reconnaissance, air units are also very effective. The are capable of patrolling at great speed - -- covering a large terrain. However, airborne recce can be vulnerable to air to ground attacks, and you need air superiority to make then - -- effective. Also the instruments available at the air units play a big role in the effectiveness of the reconnaissance. - -- Air units which have ground detection capabilities will be much more effective than air units with only visual detection capabilities. - -- For the red coalition, the Mi-28N and for the blue side, the reaper are such effective reconnaissance airborne units. - -- - -- Reconnaissance networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection} instance that is given as the first parameter to the A2G dispatcher. - -- By defining in a **smart way the names or name prefixes of the reconnaissance groups**, these groups will be **automatically added or removed** to or from the reconnaissance network, - -- when these groups are spawned in or destroyed during the ongoing battle. - -- By spawning in dynamically additional recce, you can ensure that there is sufficient reconnaissance coverage so the defense mechanism is continuously - -- alerted of new enemy ground targets. - -- - -- The following example defens a new reconnaissance network using a @{Functional.Detection#DETECTION_AREAS} object. - -- - -- -- Define a SET_GROUP object that builds a collection of groups that define the recce network. - -- -- Here we build the network with all the groups that have a name starting with CCCP Recce. - -- DetectionSetGroup = SET_GROUP:New() -- Defene a set of group objects, caled DetectionSetGroup. - -- - -- DetectionSetGroup:FilterPrefixes( { "CCCP Recce" } ) -- The DetectionSetGroup will search for groups that start with the name "CCCP Recce". - -- - -- -- This command will start the dynamic filtering, so when groups spawn in or are destroyed, - -- -- which have a group name starting with "CCCP Recce", then these will be automatically added or removed from the set. - -- DetectionSetGroup:FilterStart() - -- - -- -- This command defines the reconnaissance network. - -- -- It will group any detected ground enemy targets within a radius of 1km. - -- -- It uses the DetectionSetGroup, which defines the set of reconnaissance groups to detect for enemy ground targets. - -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 1000 ) - -- - -- -- Setup the A2A dispatcher, and initialize it. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- - -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **DetectionSetGroup**. - -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with `"CCCP Recce"` to be included in the set. - -- **DetectionSetGroup** is then calling `FilterStart()`, which is starting the dynamic filtering or inclusion of these groups. - -- Note that any destroy or new spawn of a group having a name, starting with the above prefix, will be removed or added to the set. - -- - -- Then a new detection object is created from the class `DETECTION_AREAS`. A grouping radius of 1000 meters (1km) is choosen. - -- - -- The `Detection` object is then passed to the @{#AI_A2G_DISPATCHER.New}() method to indicate the reconnaissance network - -- configuration and setup the A2G defense detection mechanism. - -- - -- ### 1.2. Setup the A2G dispatcher for both a red and blue coalition. - -- - -- Following the above described procedure, you'll need to create for each coalition an separate detection network, and a separate A2G dispatcher. - -- Ensure that while doing so, that you name the objects differently both for red and blue coalition. - -- - -- For example like this for the red coalition: - -- - -- DetectionRed = DETECTION_AREAS:New( DetectionSetGroupRed, 1000 ) - -- A2GDispatcherRed = AI_A2G_DISPATCHER:New( DetectionRed ) - -- - -- And for the blue coalition: - -- - -- DetectionBlue = DETECTION_AREAS:New( DetectionSetGroupBlue, 1000 ) - -- A2GDispatcherBlue = AI_A2G_DISPATCHER:New( DetectionBlue ) - -- - -- - -- Note: Also the SET_GROUP objects should be created for each coalition separately, containing each red and blue recce respectively! - -- - -- ### 1.3. Define the enemy ground target **grouping radius**, in case you use DETECTION_AREAS: - -- - -- The target grouping radius is a property of the DETECTION_AREAS class, that was passed to the AI_A2G_DISPATCHER:New() method, - -- but can be changed. The grouping radius should not be too small, but also depends on the types of ground forces and the way you want your mission to evolve. - -- A large radius will mean large groups of enemy ground targets, while making smaller groups will result in a more fragmented defense system. - -- Typically I suggest a grouping radius of 1km. This is the right balance to create efficient defenses. - -- - -- Note that detected targets are constantly re-grouped, that is, when certain detected enemy ground units are moving further than the group radius, - -- then these units will become a separate area being detected. This may result in additional defenses being started by the dispatcher! - -- So don't make this value too small! Again, I advise about 1km or 1000 meters. - -- - -- ## 2. Setup (a) **Defense Coordinate(s)**. - -- - -- As explained above, defense coordinates are the center of your defense operations. - -- The more threat to the defense coordinate, the higher it is likely a defensive action will be launched. - -- - -- Find below an example how to add defense coordinates: - -- - -- -- Add defense coordinates. - -- A2GDispatcher:AddDefenseCoordinate( "HQ", GROUP:FindByName( "HQ" ):GetCoordinate() ) - -- - -- In this example, the coordinate of a group called `"HQ"` is retrieved, using `:GetCoordinate()` - -- This returns a COORDINATE object, pointing to the first unit within the GROUP object. - -- - -- The method @{#AI_A2G_DISPATCHER.AddDefenseCoordinate}() adds a new defense coordinate to the `A2GDispatcher` object. - -- The first parameter is the key of the defense coordinate, the second the coordinate itself. - -- - -- Later, a COORDINATE_UNIT will be added to the framework, which can be used to assign "moving" coordinates to an A2G dispatcher. - -- - -- **REMEMBER!** - -- - -- - **Defense coordinates are the center of the A2G dispatcher defense system!** - -- - **You can define more defense coordinates to defend a larger area.** - -- - **Detected enemy ground targets are not immediately engaged, but are engaged with a reactivity or probability calculation!** - -- - -- But, there is more to it ... - -- - -- - -- ### 2.1. The **Defense Radius**. - -- - -- The defense radius defines the maximum radius that a defense will be initiated around each defense coordinate. - -- So even when there are targets further away than the defense radius, then these targets won't be engaged upon. - -- By default, the defense radius is set to 100km (100.000 meters), but can be changed using the @{#AI_A2G_DISPATCHER.SetDefenseRadius}() method. - -- Note that the defense radius influences the defense reactivity also! The larger the defense radius, the more reactive the defenses will be. - -- - -- For example: - -- - -- A2GDispatcher:SetDefenseRadius( 30000 ) - -- - -- This defines an A2G dispatcher which will engage on enemy ground targets within 30km radius around the defense coordinate. - -- Note that the defense radius **applies to all defense coordinates** defined within the A2G dispatcher. - -- - -- ### 2.2. The **Defense Reactivity**. - -- - -- There are 5 levels that can be configured to tweak the defense reactivity. As explained above, the threat to a defense coordinate is - -- also determined by the distance of the enemy ground target to the defense coordinate. - -- If you want to have a **low** defense reactivity, that is, the probability that an A2G defense will engage to the enemy ground target, then - -- use the @{#AI_A2G_DISPATCHER.SetDefenseReactivityLow}() method. For medium and high reactivity, use the methods - -- @{#AI_A2G_DISPATCHER.SetDefenseReactivityMedium}() and @{#AI_A2G_DISPATCHER.SetDefenseReactivityHigh}() respectively. - -- - -- Note that the reactivity of defenses is always in relation to the Defense Radius! the shorter the distance, - -- the less reactive the defenses will be in terms of distance to enemy ground targets! - -- - -- For example: - -- - -- A2GDispatcher:SetDefenseReactivityHigh() - -- - -- This defines an A2G dispatcher with high defense reactivity. - -- - -- ## 3. **Squadrons**. - -- - -- The A2G dispatcher works with **Squadrons**, that need to be defined using the different methods available. - -- - -- Use the method @{#AI_A2G_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, farp or carrier, - -- while defining which helicopter or plane **templates** are being used by the squadron and how many **resources** are available. - -- - -- **Multiple squadrons** can be defined within one A2G dispatcher, each having specific defense tasks and defense parameter settings! - -- - -- Squadrons: - -- - -- * Have name (string) that is the identifier or **key** of the squadron. - -- * Have specific helicopter or plane **templates**. - -- * Are located at **one** airbase, farp or carrier. - -- * Optionally have a **limited set of resources**. The default is that squadrons have **unlimited resources**. - -- - -- The name of the squadron given acts as the **squadron key** in all `A2GDispatcher:SetSquadron...()` or `A2GDispatcher:GetSquadron...()` methods. - -- - -- Additionally, squadrons have specific configuration options to: - -- - -- * Control how new helicopters or aircraft are taking off from the airfield, farp or carrier (in the air, cold, hot, at the runway). - -- * Control how returning helicopters or aircraft are landing at the airfield, farp or carrier (in the air near the airbase, after landing, after engine shutdown). - -- * Control the **grouping** of new helicopters or aircraft spawned at the airfield, farp or carrier. If there is more than one helicopter or aircraft to be spawned, these may be grouped. - -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of helicopters, planes, amount of resources and payload (weapon configuration) chosen, - -- the mission designer can choose to increase or reduce the amount of planes spawned. - -- - -- The method @{#AI_A2G_DISPATCHER.SetSquadron}() defines for you a new squadron. - -- The provided parameters are the squadron name, airbase name and a list of template prefixe, and a number that indicates the amount of resources. - -- - -- For example, this defines 3 new squadrons: - -- - -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50" }, 10 ) - -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50" }, 10 ) - -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50" }, 10 ) - -- - -- The latter 2 will depart from FARPs, which bare the name `"CAS"` and `"BAI"`. - -- - -- - -- ### 3.1. Squadrons **Tasking**. - -- - -- Squadrons can be commanded to execute 3 types of tasks, as explained above: - -- - -- - SEAD: Suppression of Air Defenses, which are ground targets that have medium or long range radar emitters. - -- - CAS : Close Air Support, when there are enemy ground targets close to friendly units. - -- - BAI : Battlefield Air Interdiction, which are targets further away from the frond-line. - -- - -- You need to configure each squadron which task types you want it to perform. Read on ... - -- - -- ### 3.2. Squadrons enemy ground target **Engagement**. - -- - -- There are two ways how targets can be engaged: directly upon call from the airfield, farp or carrier, or through a patrol. - -- - -- Patrols are extremely handy, as these will airborne your helicopters or airplanes in advance. They will patrol in defined zones outlined, - -- and will engage with the targets once commanded. If the patrol zone is close enough to the enemy ground targets, then the time required - -- to engage is heavily minimized! - -- - -- However; patrols come with a side effect: since your resources are airborne, they will be vulnerable to incoming air attacks from the enemy. - -- - -- The mission designer needs to carefully balance the need for patrols or the need for engagement on call from the airfields. - -- - -- ### 3.3. Squadron **on call engagement**. - -- - -- So to make squadrons engage targets from the airfields, use the following methods: - -- - -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSead}() method. - -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCas}() method. - -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBai}() method. - -- - -- Note that for the tasks, specific helicopter or airplane templates are required to be used, which you can configure using your mission editor. - -- Especially the payload (weapons configuration) is important to get right. - -- - -- For example, the following will define for the squadrons different tasks: - -- - -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) - -- A2GDispatcher:SetSquadronSead( "Maykop SEAD", 120, 250 ) - -- - -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) - -- A2GDispatcher:SetSquadronCas( "Maykop CAS", 120, 250 ) - -- - -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) - -- A2GDispatcher:SetSquadronBai( "Maykop BAI", 120, 250 ) - -- - -- ### 3.4. Squadron **on patrol engagement**. - -- - -- Squadrons can be setup to patrol in the air near the engagement hot zone. - -- When needed, the A2G defense units will be close to the battle area, and can engage quickly. - -- - -- So to make squadrons engage targets from a patrol zone, use the following methods: - -- - -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrol}() method. - -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrol}() method. - -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrol}() method. - -- - -- Because a patrol requires more parameters, the following methods must be used to fine-tune the patrols for each squadron. - -- - -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrolInterval}() method. - -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrolInterval}() method. - -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrolInterval}() method. - -- - -- Here an example to setup patrols of various task types: - -- - -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) - -- A2GDispatcher:SetSquadronSeadPatrol( "Maykop SEAD", PatrolZone, 300, 500, 50, 80, 250, 300 ) - -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop SEAD", 2, 30, 60, 1, "SEAD" ) - -- - -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) - -- A2GDispatcher:SetSquadronCasPatrol( "Maykop CAS", PatrolZone, 600, 700, 50, 80, 250, 300 ) - -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop CAS", 2, 30, 60, 1, "CAS" ) - -- - -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) - -- A2GDispatcher:SetSquadronBaiPatrol( "Maykop BAI", PatrolZone, 800, 900, 50, 80, 250, 300 ) - -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop BAI", 2, 30, 60, 1, "BAI" ) - -- - -- @field #AI_A2G_DISPATCHER - AI_A2G_DISPATCHER = { - ClassName = "AI_A2G_DISPATCHER", - Detection = nil, - } - - - --- List of defense coordinates. - -- @type AI_A2G_DISPATCHER.DefenseCoordinates - -- @map <#string,Core.Point#COORDINATE> A list of all defense coordinates mapped per defense coordinate name. - - --- @field #AI_A2G_DISPATCHER.DefenseCoordinates DefenseCoordinates - AI_A2G_DISPATCHER.DefenseCoordinates = {} - - --- Enumerator for spawns at airbases - -- @type AI_A2G_DISPATCHER.Takeoff - -- @extends Wrapper.Group#GROUP.Takeoff - - --- @field #AI_A2G_DISPATCHER.Takeoff Takeoff - AI_A2G_DISPATCHER.Takeoff = GROUP.Takeoff - - --- Defnes Landing location. - -- @field Landing - AI_A2G_DISPATCHER.Landing = { - NearAirbase = 1, - AtRunway = 2, - AtEngineShutdown = 3, - } - - --- AI_A2G_DISPATCHER constructor. - -- This is defining the A2G DISPATCHER for one coaliton. - -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units. - -- The Detection object is polymorphic, depending on the type of detection object choosen, the detection will work differently. - -- @param #AI_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. - -- @return #AI_A2G_DISPATCHER self - -- @usage - -- - -- -- Setup the Detection, using DETECTION_AREAS. - -- -- First define the SET of GROUPs that are defining the EWR network. - -- -- Here with prefixes DF CCCP AWACS, DF CCCP EWR. - -- DetectionSetGroup = SET_GROUP:New() - -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) - -- DetectionSetGroup:FilterStart() - -- - -- -- Define the DETECTION_AREAS, using the DetectionSetGroup, with a 30km grouping radius. - -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) - -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) -- - -- - function AI_A2G_DISPATCHER:New( Detection ) - - -- Inherits from DETECTION_MANAGER - local self = BASE:Inherit( self, DETECTION_MANAGER:New( nil, Detection ) ) -- #AI_A2G_DISPATCHER - - self.Detection = Detection -- Functional.Detection#DETECTION_AREAS - - self.Detection:FilterCategories( Unit.Category.GROUND_UNIT ) - - -- This table models the DefenderSquadron templates. - self.DefenderSquadrons = {} -- The Defender Squadrons. - self.DefenderSpawns = {} - self.DefenderTasks = {} -- The Defenders Tasks. - self.DefenderDefault = {} -- The Defender Default Settings over all Squadrons. - - -- TODO: Check detection through radar. --- self.Detection:FilterCategories( { Unit.Category.GROUND } ) --- self.Detection:InitDetectRadar( false ) --- self.Detection:InitDetectVisual( true ) --- self.Detection:SetRefreshTimeInterval( 30 ) - - self:SetDefenseRadius() - self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. - self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. - - self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Air ) - self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. - self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.NearAirbase ) - self:SetDefaultOverhead( 1 ) - self:SetDefaultGrouping( 1 ) - self:SetDefaultFuelThreshold( 0.15, 0 ) -- 15% of fuel remaining in the tank will trigger the airplane to return to base or refuel. - self:SetDefaultDamageThreshold( 0.4 ) -- When 40% of damage, go RTB. - self:SetDefaultPatrolTimeInterval( 180, 600 ) -- Between 180 and 600 seconds. - self:SetDefaultPatrolLimit( 1 ) -- Maximum one Patrol per squadron. - - - self:AddTransition( "Started", "Assign", "Started" ) - - --- OnAfter Transition Handler for Event Assign. - -- @function [parent=#AI_A2G_DISPATCHER] OnAfterAssign - -- @param #AI_A2G_DISPATCHER self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Tasking.Task_A2G#AI_A2G Task - -- @param Wrapper.Unit#UNIT TaskUnit - -- @param #string PlayerName - - self:AddTransition( "*", "Patrol", "*" ) - - --- Patrol Handler OnBefore for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] OnBeforePatrol - -- @param #AI_A2G_DISPATCHER self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Patrol Handler OnAfter for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] OnAfterPatrol - -- @param #AI_A2G_DISPATCHER self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Patrol Trigger for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] Patrol - -- @param #AI_A2G_DISPATCHER self - - --- Patrol Asynchronous Trigger for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] __Patrol - -- @param #AI_A2G_DISPATCHER self - -- @param #number Delay - - self:AddTransition( "*", "Defend", "*" ) - - --- Defend Handler OnBefore for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeDefend - -- @param #AI_A2G_DISPATCHER self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Defend Handler OnAfter for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] OnAfterDefend - -- @param #AI_A2G_DISPATCHER self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Defend Trigger for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] Defend - -- @param #AI_A2G_DISPATCHER self - - --- Defend Asynchronous Trigger for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] __Defend - -- @param #AI_A2G_DISPATCHER self - -- @param #number Delay - - self:AddTransition( "*", "Engage", "*" ) - - --- Engage Handler OnBefore for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeEngage - -- @param #AI_A2G_DISPATCHER self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Engage Handler OnAfter for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] OnAfterEngage - -- @param #AI_A2G_DISPATCHER self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Engage Trigger for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] Engage - -- @param #AI_A2G_DISPATCHER self - - --- Engage Asynchronous Trigger for AI_A2G_DISPATCHER - -- @function [parent=#AI_A2G_DISPATCHER] __Engage - -- @param #AI_A2G_DISPATCHER self - -- @param #number Delay - - - -- Subscribe to the CRASH event so that when planes are shot - -- by a Unit from the dispatcher, they will be removed from the detection... - -- This will avoid the detection to still "know" the shot unit until the next detection. - -- Otherwise, a new defense or engage may happen for an already shot plane! - - - self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) - self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) - --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) - - - self:HandleEvent( EVENTS.Land ) - self:HandleEvent( EVENTS.EngineShutdown ) - - -- Handle the situation where the airbases are captured. - self:HandleEvent( EVENTS.BaseCaptured ) - - self:SetTacticalDisplay( false ) - - self.DefenderPatrolIndex = 0 - - self:SetDefenseReactivityMedium() - - self:__Start( 5 ) - - return self - end - - - --- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:onafterStart( From, Event, To ) - - self:GetParent( self ).onafterStart( self, From, Event, To ) - - -- Spawn the resources. - for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do - DefenderSquadron.Resource = {} - for Resource = 1, DefenderSquadron.ResourceCount or 0 do - self:ParkDefender( DefenderSquadron ) - end - end - end - - - --- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:ParkDefender( DefenderSquadron ) - local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) - local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN - Spawn:InitGrouping( 1 ) - local SpawnGroup - if self:IsSquadronVisible( DefenderSquadron.Name ) then - SpawnGroup = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, SPAWN.Takeoff.Cold ) - local GroupName = SpawnGroup:GetName() - DefenderSquadron.Resources = DefenderSquadron.Resources or {} - DefenderSquadron.Resources[TemplateID] = DefenderSquadron.Resources[TemplateID] or {} - DefenderSquadron.Resources[TemplateID][GroupName] = {} - DefenderSquadron.Resources[TemplateID][GroupName] = SpawnGroup - end - end - - - --- @param #AI_A2G_DISPATCHER self - -- @param Core.Event#EVENTDATA EventData - function AI_A2G_DISPATCHER:OnEventBaseCaptured( EventData ) - - local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured. - - self:I( "Captured " .. AirbaseName ) - - -- Now search for all squadrons located at the airbase, and sanatize them. - for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do - if Squadron.AirbaseName == AirbaseName then - Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. - Squadron.Captured = true - self:I( "Squadron " .. SquadronName .. " captured." ) - end - end - end - - --- @param #AI_A2G_DISPATCHER self - -- @param Core.Event#EVENTDATA EventData - function AI_A2G_DISPATCHER:OnEventCrashOrDead( EventData ) - self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) - end - - --- @param #AI_A2G_DISPATCHER self - -- @param Core.Event#EVENTDATA EventData - function AI_A2G_DISPATCHER:OnEventLand( EventData ) - self:F( "Landed" ) - local DefenderUnit = EventData.IniUnit - local Defender = EventData.IniGroup - local Squadron = self:GetSquadronFromDefender( Defender ) - if Squadron then - self:F( { SquadronName = Squadron.Name } ) - local LandingMethod = self:GetSquadronLanding( Squadron.Name ) - if LandingMethod == AI_A2G_DISPATCHER.Landing.AtRunway then - local DefenderSize = Defender:GetSize() - if DefenderSize == 1 then - self:RemoveDefenderFromSquadron( Squadron, Defender ) - end - DefenderUnit:Destroy() - self:ParkDefender( Squadron, Defender ) - return - end - if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then - -- Damaged units cannot be repaired anymore. - DefenderUnit:Destroy() - return - end - end - end - - --- @param #AI_A2G_DISPATCHER self - -- @param Core.Event#EVENTDATA EventData - function AI_A2G_DISPATCHER:OnEventEngineShutdown( EventData ) - local DefenderUnit = EventData.IniUnit - local Defender = EventData.IniGroup - local Squadron = self:GetSquadronFromDefender( Defender ) - if Squadron then - self:F( { SquadronName = Squadron.Name } ) - local LandingMethod = self:GetSquadronLanding( Squadron.Name ) - if LandingMethod == AI_A2G_DISPATCHER.Landing.AtEngineShutdown and - not DefenderUnit:InAir() then - local DefenderSize = Defender:GetSize() - if DefenderSize == 1 then - self:RemoveDefenderFromSquadron( Squadron, Defender ) - end - DefenderUnit:Destroy() - self:ParkDefender( Squadron, Defender ) - end - end - end - - do -- Manage the defensive behaviour - - --- @param #AI_A2G_DISPATCHER self - -- @param #string DefenseCoordinateName The name of the coordinate to be defended by A2G defenses. - -- @param Core.Point#COORDINATE DefenseCoordinate The coordinate to be defended by A2G defenses. - function AI_A2G_DISPATCHER:AddDefenseCoordinate( DefenseCoordinateName, DefenseCoordinate ) - self.DefenseCoordinates[DefenseCoordinateName] = DefenseCoordinate - end - - --- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:SetDefenseReactivityLow() - self.DefenseReactivity = 0.05 - end - - --- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:SetDefenseReactivityMedium() - self.DefenseReactivity = 0.15 - end - - --- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:SetDefenseReactivityHigh() - self.DefenseReactivity = 0.5 - end - - end - - --- Define the radius to engage any target by airborne friendlies, which are executing cap or returning from an defense mission. - -- If there is a target area detected and reported, then any friendlies that are airborne near this target area, - -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). - -- - -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, - -- will be considered to receive the command to engage that target area. - -- - -- You need to evaluate the value of this parameter carefully: - -- - -- * If too small, more defense missions may be triggered upon detected target areas. - -- * If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. - -- - -- **Use the method @{#AI_A2G_DISPATCHER.SetEngageRadius}() to modify the default Engage Radius for ALL squadrons.** - -- - -- Demonstration Mission: [AID-019 - AI_A2G - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2G%20-%20Engage%20Range%20Test) - -- - -- @param #AI_A2G_DISPATCHER self - -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Set 50km as the radius to engage any target by airborne friendlies. - -- A2GDispatcher:SetEngageRadius( 50000 ) - -- - -- -- Set 100km as the radius to engage any target by airborne friendlies. - -- A2GDispatcher:SetEngageRadius() -- 100000 is the default value. - -- - function AI_A2G_DISPATCHER:SetEngageRadius( EngageRadius ) - - --self.Detection:SetFriendliesRange( EngageRadius or 100000 ) - - return self - end - - --- Define the radius to disengage any target when the distance to the home base is larger than the specified meters. - -- @param #AI_A2G_DISPATCHER self - -- @param #number DisengageRadius (Optional, Default = 300000) The radius to disengage a target when too far from the home base. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Set 50km as the Disengage Radius. - -- A2GDispatcher:SetDisengageRadius( 50000 ) - -- - -- -- Set 100km as the Disengage Radius. - -- A2GDispatcher:SetDisngageRadius() -- 300000 is the default value. - -- - function AI_A2G_DISPATCHER:SetDisengageRadius( DisengageRadius ) - - self.DisengageRadius = DisengageRadius or 300000 - - return self - end - - - --- Define the defense radius to check if a target can be engaged by a squadron group for SEAD, CAS or BAI for defense. - -- When targets are detected that are still really far off, you don't want the AI_A2G_DISPATCHER to launch defenders, as they might need to travel too far. - -- You want it to wait until a certain defend radius is reached, which is calculated as: - -- 1. the **distance of the closest airbase to target**, being smaller than the **Defend Radius**. - -- 2. the **distance to any defense reference point**. - -- - -- The **default** defense radius is defined as **400000** or **40km**. Override the default defense radius when the era of the warfare is early, or, - -- when you don't want to let the AI_A2G_DISPATCHER react immediately when a certain border or area is not being crossed. - -- - -- Use the method @{#AI_A2G_DISPATCHER.SetDefendRadius}() to set a specific defend radius for all squadrons, - -- **the Defense Radius is defined for ALL squadrons which are operational.** - -- - -- @param #AI_A2G_DISPATCHER self - -- @param #number DefenseRadius (Optional, Default = 200000) The defense radius to engage detected targets from the nearest capable and available squadron airbase. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- -- Set 100km as the radius to defend from detected targets from the nearest airbase. - -- A2GDispatcher:SetDefendRadius( 100000 ) - -- - -- -- Set 200km as the radius to defend. - -- A2GDispatcher:SetDefendRadius() -- 200000 is the default value. - -- - function AI_A2G_DISPATCHER:SetDefenseRadius( DefenseRadius ) - - self.DefenseRadius = DefenseRadius or 100000 - - self.Detection:SetAcceptRange( self.DefenseRadius ) - - return self - end - - - - --- Define a border area to simulate a **cold war** scenario. - -- A **cold war** is one where Patrol aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. - -- A **hot war** is one where Patrol aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send Patrol and GCI aircraft to attack it. - -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this. - -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1 - -- @param #AI_A2G_DISPATCHER self - -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- -- Set one ZONE_POLYGON object as the border for the A2G dispatcher. - -- local BorderZone = ZONE_POLYGON( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- The GROUP object is a late activate helicopter unit. - -- A2GDispatcher:SetBorderZone( BorderZone ) - -- - -- or - -- - -- -- Set two ZONE_POLYGON objects as the border for the A2G dispatcher. - -- local BorderZone1 = ZONE_POLYGON( "CCCP Border1", GROUP:FindByName( "CCCP Border1" ) ) -- The GROUP object is a late activate helicopter unit. - -- local BorderZone2 = ZONE_POLYGON( "CCCP Border2", GROUP:FindByName( "CCCP Border2" ) ) -- The GROUP object is a late activate helicopter unit. - -- A2GDispatcher:SetBorderZone( { BorderZone1, BorderZone2 } ) - -- - -- - function AI_A2G_DISPATCHER:SetBorderZone( BorderZone ) - - self.Detection:SetAcceptZones( BorderZone ) - - return self - end - - --- Display a tactical report every 30 seconds about which aircraft are: - -- * Patrolling - -- * Engaging - -- * Returning - -- * Damaged - -- * Out of Fuel - -- * ... - -- @param #AI_A2G_DISPATCHER self - -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the Tactical Display for debug mode. - -- A2GDispatcher:SetTacticalDisplay( true ) - -- - function AI_A2G_DISPATCHER:SetTacticalDisplay( TacticalDisplay ) - - self.TacticalDisplay = TacticalDisplay - - return self - end - - - --- Set the default damage treshold when defenders will RTB. - -- The default damage treshold is by default set to 40%, which means that when the airplane is 40% damaged, it will go RTB. - -- @param #AI_A2G_DISPATCHER self - -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the %-tage of the damage treshold before going RTB. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the default damage treshold. - -- A2GDispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. - -- - function AI_A2G_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold ) - - self.DefenderDefault.DamageThreshold = DamageThreshold - - return self - end - - - --- Set the default Patrol time interval for squadrons, which will be used to determine a random Patrol timing. - -- The default Patrol time interval is between 180 and 600 seconds. - -- @param #AI_A2G_DISPATCHER self - -- @param #number PatrolMinSeconds The minimum amount of seconds for the random time interval. - -- @param #number PatrolMaxSeconds The maximum amount of seconds for the random time interval. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the default Patrol time interval. - -- A2GDispatcher:SetDefaultPatrolTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. - -- - function AI_A2G_DISPATCHER:SetDefaultPatrolTimeInterval( PatrolMinSeconds, PatrolMaxSeconds ) - - self.DefenderDefault.PatrolMinSeconds = PatrolMinSeconds - self.DefenderDefault.PatrolMaxSeconds = PatrolMaxSeconds - - return self - end - - - --- Set the default Patrol limit for squadrons, which will be used to determine how many Patrol can be airborne at the same time for the squadron. - -- The default Patrol limit is 1 Patrol, which means one Patrol group being spawned. - -- @param #AI_A2G_DISPATCHER self - -- @param #number PatrolLimit The maximum amount of Patrol that can be airborne at the same time for the squadron. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the default Patrol limit. - -- A2GDispatcher:SetDefaultPatrolLimit( 2 ) -- Maximum 2 Patrol per squadron. - -- - function AI_A2G_DISPATCHER:SetDefaultPatrolLimit( PatrolLimit ) - - self.DefenderDefault.PatrolLimit = PatrolLimit - - return self - end - - - --- Set the default engage limit for squadrons, which will be used to determine how many air units will engage at the same time with the enemy. - -- The default eatrol limit is 1, which means one eatrol group maximum per squadron. - -- @param #AI_A2G_DISPATCHER self - -- @param #number EngageLimit The maximum engages that can be done at the same time per squadron. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the default Patrol limit. - -- A2GDispatcher:SetDefaultEngageLimit( 2 ) -- Maximum 2 engagements with the enemy per squadron. - -- - function AI_A2G_DISPATCHER:SetDefaultEngageLimit( EngageLimit ) - - self.DefenderDefault.EngageLimit = EngageLimit - - return self - end - - - function AI_A2G_DISPATCHER:SetIntercept( InterceptDelay ) - - self.DefenderDefault.InterceptDelay = InterceptDelay - - local Detection = self.Detection -- Functional.Detection#DETECTION_AREAS - Detection:SetIntercept( true, InterceptDelay ) - - return self - end - - - --- Calculates which defender friendlies are nearby the area, to help protect the area. - -- @param #AI_A2G_DISPATCHER self - -- @param DetectedItem - -- @return #table A list of the defender friendlies nearby, sorted by distance. - function AI_A2G_DISPATCHER:GetDefenderFriendliesNearBy( DetectedItem ) - --- local DefenderFriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem ) - - local DefenderFriendliesNearBy = {} - - local DetectionCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) - - local ScanZone = ZONE_RADIUS:New( "ScanZone", DetectionCoordinate:GetVec2(), self.DefenseRadius ) - - ScanZone:Scan( Object.Category.UNIT, { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - - local DefenderUnits = ScanZone:GetScannedUnits() - - for DefenderUnitID, DefenderUnit in pairs( DefenderUnits ) do - local DefenderUnit = UNIT:FindByName( DefenderUnit:getName() ) - - DefenderFriendliesNearBy[#DefenderFriendliesNearBy+1] = DefenderUnit - end - - - return DefenderFriendliesNearBy - end - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:GetDefenderTasks() - return self.DefenderTasks or {} - end - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:GetDefenderTask( Defender ) - return self.DefenderTasks[Defender] - end - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:GetDefenderTaskFsm( Defender ) - return self:GetDefenderTask( Defender ).Fsm - end - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:GetDefenderTaskTarget( Defender ) - return self:GetDefenderTask( Defender ).Target - end - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:GetDefenderTaskSquadronName( Defender ) - return self:GetDefenderTask( Defender ).SquadronName - end - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:ClearDefenderTask( Defender ) - if Defender:IsAlive() and self.DefenderTasks[Defender] then - local Target = self.DefenderTasks[Defender].Target - local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " - Message = Message .. Defender:GetName() - if Target then - Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" - end - self:F( { Target = Message } ) - end - self.DefenderTasks[Defender] = nil - return self - end - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:ClearDefenderTaskTarget( Defender ) - - local DefenderTask = self:GetDefenderTask( Defender ) - - if Defender:IsAlive() and DefenderTask then - local Target = DefenderTask.Target - local Message = "Clearing (" .. DefenderTask.Type .. ") " - Message = Message .. Defender:GetName() - if Target then - Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" - end - self:F( { Target = Message } ) - end - if Defender and DefenderTask and DefenderTask.Target then - DefenderTask.Target = nil - end --- if Defender and DefenderTask then --- if DefenderTask.Fsm:Is( "Fuel" ) --- or DefenderTask.Fsm:Is( "LostControl") --- or DefenderTask.Fsm:Is( "Damaged" ) then --- self:ClearDefenderTask( Defender ) --- end --- end - return self - end - - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target, Size ) - - self:F( { SquadronName = SquadronName, Defender = Defender:GetName() } ) - - self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {} - self.DefenderTasks[Defender].Type = Type - self.DefenderTasks[Defender].Fsm = Fsm - self.DefenderTasks[Defender].SquadronName = SquadronName - self.DefenderTasks[Defender].Size = Size - - if Target then - self:SetDefenderTaskTarget( Defender, Target ) - end - return self - end - - - --- - -- @param #AI_A2G_DISPATCHER self - -- @param Wrapper.Group#GROUP AIGroup - function AI_A2G_DISPATCHER:SetDefenderTaskTarget( Defender, AttackerDetection ) - - local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " - Message = Message .. Defender:GetName() - Message = Message .. ( AttackerDetection and ( " target " .. AttackerDetection.Index .. " [" .. AttackerDetection.Set:Count() .. "]" ) ) or "" - self:F( { AttackerDetection = Message } ) - if AttackerDetection then - self.DefenderTasks[Defender].Target = AttackerDetection - end - return self - end - - - --- This is the main method to define Squadrons programmatically. - -- Squadrons: - -- - -- * Have a **name or key** that is the identifier or key of the squadron. - -- * Have **specific plane types** defined by **templates**. - -- * Are **located at one specific airbase**. Multiple squadrons can be located at one airbase through. - -- * Optionally have a limited set of **resources**. The default is that squadrons have unlimited resources. - -- - -- The name of the squadron given acts as the **squadron key** in the AI\_A2G\_DISPATCHER:Squadron...() methods. - -- - -- Additionally, squadrons have specific configuration options to: - -- - -- * Control how new aircraft are **taking off** from the airfield (in the air, cold, hot, at the runway). - -- * Control how returning aircraft are **landing** at the airfield (in the air near the airbase, after landing, after engine shutdown). - -- * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped. - -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned. - -- - -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft. - -- - -- @param #AI_A2G_DISPATCHER self - -- - -- @param #string SquadronName A string (text) that defines the squadron identifier or the key of the Squadron. - -- It can be any name, for example `"104th Squadron"` or `"SQ SQUADRON1"`, whatever. - -- As long as you remember that this name becomes the identifier of your squadron you have defined. - -- You need to use this name in other methods too! - -- - -- @param #string AirbaseName The airbase name where you want to have the squadron located. - -- You need to specify here EXACTLY the name of the airbase as you see it in the mission editor. - -- Examples are `"Batumi"` or `"Tbilisi-Lochini"`. - -- EXACTLY the airbase name, between quotes `""`. - -- To ease the airbase naming when using the LDT editor and IntelliSense, the @{Wrapper.Airbase#AIRBASE} class contains enumerations of the airbases of each map. - -- - -- * Caucasus: @{Wrapper.Airbase#AIRBASE.Caucaus} - -- * Nevada or NTTR: @{Wrapper.Airbase#AIRBASE.Nevada} - -- * Normandy: @{Wrapper.Airbase#AIRBASE.Normandy} - -- - -- @param #string TemplatePrefixes A string or an array of strings specifying the **prefix names of the templates** (not going to explain what is templates here again). - -- Examples are `{ "104th", "105th" }` or `"104th"` or `"Template 1"` or `"BLUE PLANES"`. - -- Just remember that your template (groups late activated) need to start with the prefix you have specified in your code. - -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets. - -- - -- @param #number ResourceCount (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. - -- - -- @usage - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- @usage - -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... - -- A2GDispatcher:SetSquadron( "Squadron1", "Batumi", "SQ1", 40 ) - -- - -- @usage - -- -- This will create squadron "Sq 1" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" and has 20 planes in stock... - -- -- Note that in this implementation, the A2G dispatcher will select a random plane type when a new plane (group) needs to be spawned for defenses. - -- -- Note the usage of the {} for the airplane templates list. - -- A2GDispatcher:SetSquadron( "Sq 1", "Batumi", { "Mig-29", "Su-27" }, 40 ) - -- - -- @usage - -- -- This will create 2 squadrons "104th" and "23th" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" respectively and each squadron has 10 planes in stock... - -- A2GDispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 ) - -- A2GDispatcher:SetSquadron( "23th", "Batumi", "Su-27", 10 ) - -- - -- @usage - -- -- This is an example like the previous, but now with infinite resources. - -- -- The ResourceCount parameter is not given in the SetSquadron method. - -- A2GDispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) - -- A2GDispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) - -- - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) - - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - - local DefenderSquadron = self.DefenderSquadrons[SquadronName] - - DefenderSquadron.Name = SquadronName - DefenderSquadron.Airbase = AIRBASE:FindByName( AirbaseName ) - DefenderSquadron.AirbaseName = DefenderSquadron.Airbase:GetName() - if not DefenderSquadron.Airbase then - error( "Cannot find airbase with name:" .. AirbaseName ) - end - - DefenderSquadron.Spawn = {} - if type( TemplatePrefixes ) == "string" then - local SpawnTemplate = TemplatePrefixes - self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) - DefenderSquadron.Spawn[1] = self.DefenderSpawns[SpawnTemplate] - else - for TemplateID, SpawnTemplate in pairs( TemplatePrefixes ) do - self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) - DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate] - end - end - DefenderSquadron.ResourceCount = ResourceCount - DefenderSquadron.TemplatePrefixes = TemplatePrefixes - DefenderSquadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured. - - self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) - - return self - end - - --- Get an item from the Squadron table. - -- @param #AI_A2G_DISPATCHER self - -- @return #table - function AI_A2G_DISPATCHER:GetSquadron( SquadronName ) - - local DefenderSquadron = self.DefenderSquadrons[SquadronName] - - if not DefenderSquadron then - error( "Unknown Squadron:" .. SquadronName ) - end - - return DefenderSquadron - end - - - --- Set the Squadron visible before startup of the dispatcher. - -- All planes will be spawned as uncontrolled on the parking spot. - -- They will lock the parking spot. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Set the Squadron visible before startup of dispatcher. - -- A2GDispatcher:SetSquadronVisible( "Mineralnye" ) - -- - function AI_A2G_DISPATCHER:SetSquadronVisible( SquadronName ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.Uncontrolled = true - - for SpawnTemplate, DefenderSpawn in pairs( self.DefenderSpawns ) do - DefenderSpawn:InitUnControlled() - end - - end - - --- Check if the Squadron is visible before startup of the dispatcher. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @return #bool true if visible. - -- @usage - -- - -- -- Set the Squadron visible before startup of dispatcher. - -- local IsVisible = A2GDispatcher:IsSquadronVisible( "Mineralnye" ) - -- - function AI_A2G_DISPATCHER:IsSquadronVisible( SquadronName ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - if DefenderSquadron then - return DefenderSquadron.Uncontrolled == true - end - - return nil - - end - - - --- Set the squadron patrol parameters for a specific task type. - -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. - -- - -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for SEAD tasks. - -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for CAS tasks. - -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for BAI tasks. - -- - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. - -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. - -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. - -- @param #number Probability Is not in use, you can skip this parameter. - -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Patrol Squadron execution. - -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) - -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- A2GDispatcher:SetSquadronPatrolInterval( "Mineralnye", 2, 30, 60, 1, "SEAD" ) - -- - function AI_A2G_DISPATCHER:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, DefenseTaskType ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - local Patrol = DefenderSquadron[DefenseTaskType] - if Patrol then - Patrol.LowInterval = LowInterval or 180 - Patrol.HighInterval = HighInterval or 600 - Patrol.Probability = Probability or 1 - Patrol.PatrolLimit = PatrolLimit or 1 - Patrol.Scheduler = Patrol.Scheduler or SCHEDULER:New( self ) - local Scheduler = Patrol.Scheduler -- Core.Scheduler#SCHEDULER - local ScheduleID = Patrol.ScheduleID - local Variance = ( Patrol.HighInterval - Patrol.LowInterval ) / 2 - local Repeat = Patrol.LowInterval + Variance - local Randomization = Variance / Repeat - local Start = math.random( 1, Patrol.HighInterval ) - - if ScheduleID then - Scheduler:Stop( ScheduleID ) - end - - Patrol.ScheduleID = Scheduler:Schedule( self, self.SchedulerPatrol, { SquadronName }, Start, Repeat, Randomization ) - else - error( "This squadron does not exist:" .. SquadronName ) - end - - end - - - - --- Set the squadron Patrol parameters for SEAD tasks. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. - -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. - -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. - -- @param #number Probability Is not in use, you can skip this parameter. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Patrol Squadron execution. - -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) - -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- A2GDispatcher:SetSquadronSeadPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) - -- - function AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) - - self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "SEAD" ) - - end - - - --- Set the squadron Patrol parameters for CAS tasks. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. - -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. - -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. - -- @param #number Probability Is not in use, you can skip this parameter. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Patrol Squadron execution. - -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) - -- A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- A2GDispatcher:SetSquadronCasPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) - -- - function AI_A2G_DISPATCHER:SetSquadronCasPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) - - self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "CAS" ) - - end - - - --- Set the squadron Patrol parameters for BAI tasks. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. - -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. - -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. - -- @param #number Probability Is not in use, you can skip this parameter. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Patrol Squadron execution. - -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) - -- A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- A2GDispatcher:SetSquadronBaiPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) - -- - function AI_A2G_DISPATCHER:SetSquadronBaiPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) - - self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "BAI" ) - - end - - - --- - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:GetPatrolDelay( SquadronName ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Patrol = self.DefenderSquadrons[SquadronName].Patrol or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - local Patrol = self.DefenderSquadrons[SquadronName].Patrol - if Patrol then - return math.random( Patrol.LowInterval, Patrol.HighInterval ) - else - error( "This squadron does not exist:" .. SquadronName ) - end - end - - --- - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @return #table DefenderSquadron - function AI_A2G_DISPATCHER:CanPatrol( SquadronName, DefenseTaskType ) - self:F({SquadronName = SquadronName}) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - if DefenderSquadron.Captured == false then -- We can only spawn new Patrol if the base has not been captured. - - if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. - - local Patrol = DefenderSquadron[DefenseTaskType] - if Patrol and Patrol.Patrol == true then - local PatrolCount = self:CountPatrolAirborne( SquadronName, DefenseTaskType ) - self:F( { PatrolCount = PatrolCount, PatrolLimit = Patrol.PatrolLimit, PatrolProbability = Patrol.Probability } ) - if PatrolCount < Patrol.PatrolLimit then - local Probability = math.random() - if Probability <= Patrol.Probability then - return DefenderSquadron, Patrol - end - end - else - self:F( "No patrol for " .. SquadronName ) - end - end - end - return nil - end - - - --- - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @return #table DefenderSquadron - function AI_A2G_DISPATCHER:CanDefend( SquadronName, DefenseTaskType ) - self:F({SquadronName = SquadronName, DefenseTaskType}) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - if DefenderSquadron.Captured == false then -- We can only spawn new defense if the home airbase has not been captured. - - if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. - if DefenderSquadron[DefenseTaskType] and ( DefenderSquadron[DefenseTaskType].Defend == true ) then - return DefenderSquadron, DefenderSquadron[DefenseTaskType] - end - end - end - return nil - end - - --- Set the squadron engage limit for a specific task type. - -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. - -- - -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for SEAD tasks. - -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for CAS tasks. - -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for BAI tasks. - -- - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. - -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Patrol Squadron execution. - -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) - -- A2GDispatcher:SetSquadronEngageLimit( "Mineralnye", 2, "SEAD" ) -- Engage maximum 2 groups with the enemy for SEAD defense. - -- - function AI_A2G_DISPATCHER:SetSquadronEngageLimit( SquadronName, EngageLimit, DefenseTaskType ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - local Defense = DefenderSquadron[DefenseTaskType] - if Defense then - Defense.EngageLimit = EngageLimit or 1 - else - error( "This squadron does not exist:" .. SquadronName ) - end - - end - - - - - --- - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number EngageMinSpeed The minimum speed at which the SEAD task can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the SEAD task can be executed. - -- @usage - -- - -- -- SEAD Squadron execution. - -- A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200 ) - -- A2GDispatcher:SetSquadronSead( "Novo", 900, 2100 ) - -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200 ) - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronSead( SquadronName, EngageMinSpeed, EngageMaxSpeed ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} - - local Sead = DefenderSquadron.SEAD - Sead.Name = SquadronName - Sead.EngageMinSpeed = EngageMinSpeed - Sead.EngageMaxSpeed = EngageMaxSpeed - Sead.Defend = true - - self:F( { Sead = Sead } ) - end - - --- Set the squadron SEAD engage limit. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Patrol Squadron execution. - -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) - -- A2GDispatcher:SetSquadronSeadEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for SEAD defense. - -- - function AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit( SquadronName, EngageLimit ) - - self:SetSquadronEngageLimit( SquadronName, EngageLimit, "SEAD" ) - - end - - - - - --- Set a Sead patrol for a Squadron. - -- The Sead patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. - -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. - -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. - -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. - -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. - -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. - -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Sead Patrol Squadron execution. - -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) - -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- - function AI_A2G_DISPATCHER:SetSquadronSeadPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} - - local SeadPatrol = DefenderSquadron.SEAD - SeadPatrol.Name = SquadronName - SeadPatrol.Zone = Zone - SeadPatrol.FloorAltitude = FloorAltitude - SeadPatrol.CeilingAltitude = CeilingAltitude - SeadPatrol.PatrolMinSpeed = PatrolMinSpeed - SeadPatrol.PatrolMaxSpeed = PatrolMaxSpeed - SeadPatrol.EngageMinSpeed = EngageMinSpeed - SeadPatrol.EngageMaxSpeed = EngageMaxSpeed - SeadPatrol.AltType = AltType - SeadPatrol.Patrol = true - - self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "SEAD" ) - - self:F( { Sead = SeadPatrol } ) - end - - - --- - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number EngageMinSpeed The minimum speed at which the CAS task can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the CAS task can be executed. - -- @usage - -- - -- -- CAS Squadron execution. - -- A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200 ) - -- A2GDispatcher:SetSquadronCas( "Novo", 900, 2100 ) - -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200 ) - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronCas( SquadronName, EngageMinSpeed, EngageMaxSpeed ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.CAS = DefenderSquadron.CAS or {} - - local Cas = DefenderSquadron.CAS - Cas.Name = SquadronName - Cas.EngageMinSpeed = EngageMinSpeed - Cas.EngageMaxSpeed = EngageMaxSpeed - Cas.Defend = true - - self:F( { Cas = Cas } ) - end - - - --- Set the squadron CAS engage limit. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Patrol Squadron execution. - -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) - -- A2GDispatcher:SetSquadronCasEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for CAS defense. - -- - function AI_A2G_DISPATCHER:SetSquadronCasEngageLimit( SquadronName, EngageLimit ) - - self:SetSquadronEngageLimit( SquadronName, EngageLimit, "CAS" ) - - end - - - - - --- Set a Cas patrol for a Squadron. - -- The Cas patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. - -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. - -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. - -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. - -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. - -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. - -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Cas Patrol Squadron execution. - -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) - -- A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- - function AI_A2G_DISPATCHER:SetSquadronCasPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.CAS = DefenderSquadron.CAS or {} - - local CasPatrol = DefenderSquadron.CAS - CasPatrol.Name = SquadronName - CasPatrol.Zone = Zone - CasPatrol.FloorAltitude = FloorAltitude - CasPatrol.CeilingAltitude = CeilingAltitude - CasPatrol.PatrolMinSpeed = PatrolMinSpeed - CasPatrol.PatrolMaxSpeed = PatrolMaxSpeed - CasPatrol.EngageMinSpeed = EngageMinSpeed - CasPatrol.EngageMaxSpeed = EngageMaxSpeed - CasPatrol.AltType = AltType - CasPatrol.Patrol = true - - self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "CAS" ) - - self:F( { Cas = CasPatrol } ) - end - - - --- - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number EngageMinSpeed The minimum speed at which the BAI task can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the BAI task can be executed. - -- @usage - -- - -- -- BAI Squadron execution. - -- A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200 ) - -- A2GDispatcher:SetSquadronBai( "Novo", 900, 2100 ) - -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200 ) - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronBai( SquadronName, EngageMinSpeed, EngageMaxSpeed ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.BAI = DefenderSquadron.BAI or {} - - local Bai = DefenderSquadron.BAI - Bai.Name = SquadronName - Bai.EngageMinSpeed = EngageMinSpeed - Bai.EngageMaxSpeed = EngageMaxSpeed - Bai.Defend = true - - self:F( { Bai = Bai } ) - end - - - --- Set the squadron BAI engage limit. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Patrol Squadron execution. - -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) - -- A2GDispatcher:SetSquadronBaiEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for BAI defense. - -- - function AI_A2G_DISPATCHER:SetSquadronBaiEngageLimit( SquadronName, EngageLimit ) - - self:SetSquadronEngageLimit( SquadronName, EngageLimit, "BAI" ) - - end - - - --- Set a Bai patrol for a Squadron. - -- The Bai patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. - -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. - -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. - -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. - -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. - -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. - -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Bai Patrol Squadron execution. - -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) - -- A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) - -- - function AI_A2G_DISPATCHER:SetSquadronBaiPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.BAI = DefenderSquadron.BAI or {} - - local BaiPatrol = DefenderSquadron.BAI - BaiPatrol.Name = SquadronName - BaiPatrol.Zone = Zone - BaiPatrol.FloorAltitude = FloorAltitude - BaiPatrol.CeilingAltitude = CeilingAltitude - BaiPatrol.PatrolMinSpeed = PatrolMinSpeed - BaiPatrol.PatrolMaxSpeed = PatrolMaxSpeed - BaiPatrol.EngageMinSpeed = EngageMinSpeed - BaiPatrol.EngageMaxSpeed = EngageMaxSpeed - BaiPatrol.AltType = AltType - BaiPatrol.Patrol = true - - self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "BAI" ) - - self:F( { Bai = BaiPatrol } ) - end - - - --- Defines the default amount of extra planes that will take-off as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. - -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, - -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... - -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. - -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: - -- - -- * Higher than 1, will increase the defense unit amounts. - -- * Lower than 1, will decrease the defense unit amounts. - -- - -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group - -- multiplied by the Overhead and rounded up to the smallest integer. - -- - -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. - -- - -- See example below. - -- - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. - -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. - -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. - -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. - -- - -- A2GDispatcher:SetDefaultOverhead( 1.5 ) - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetDefaultOverhead( Overhead ) - - self.DefenderDefault.Overhead = Overhead - - return self - end - - - --- Defines the amount of extra planes that will take-off as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. - -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, - -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... - -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. - -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: - -- - -- * Higher than 1, will increase the defense unit amounts. - -- * Lower than 1, will decrease the defense unit amounts. - -- - -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group - -- multiplied by the Overhead and rounded up to the smallest integer. - -- - -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. - -- - -- See example below. - -- - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. - -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. - -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. - -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. - -- - -- A2GDispatcher:SetSquadronOverhead( "SquadronName", 1.5 ) - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.Overhead = Overhead - - return self - end - - - --- Gets the overhead of planes as part of the defense system, in comparison with the attackers. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @return #number The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. - -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, - -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... - -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. - -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: - -- - -- * Higher than 1, will increase the defense unit amounts. - -- * Lower than 1, will decrease the defense unit amounts. - -- - -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group - -- multiplied by the Overhead and rounded up to the smallest integer. - -- - -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. - -- - -- See example below. - -- - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. - -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. - -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. - -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. - -- - -- local SquadronOverhead = A2GDispatcher:GetSquadronOverhead( "SquadronName" ) - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:GetSquadronOverhead( SquadronName ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - return DefenderSquadron.Overhead or self.DefenderDefault.Overhead - end - - - --- Sets the default grouping of new airplanes spawned. - -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. - -- @param #AI_A2G_DISPATCHER self - -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Set a grouping by default per 2 airplanes. - -- A2GDispatcher:SetDefaultGrouping( 2 ) - -- - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetDefaultGrouping( Grouping ) - - self.DefenderDefault.Grouping = Grouping - - return self - end - - - --- Sets the grouping of new airplanes spawned. - -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Set a grouping per 2 airplanes. - -- A2GDispatcher:SetSquadronGrouping( "SquadronName", 2 ) - -- - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.Grouping = Grouping - - return self - end - - - --- Defines the default method at which new flights will spawn and take-off as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default take-off in the air. - -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Air ) - -- - -- -- Let new flights by default take-off from the runway. - -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Runway ) - -- - -- -- Let new flights by default take-off from the airbase hot. - -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Hot ) - -- - -- -- Let new flights by default take-off from the airbase cold. - -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Cold ) - -- - -- - -- @return #AI_A2G_DISPATCHER - -- - function AI_A2G_DISPATCHER:SetDefaultTakeoff( Takeoff ) - - self.DefenderDefault.Takeoff = Takeoff - - return self - end - - --- Defines the method at which new flights will spawn and take-off as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off in the air. - -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Air ) - -- - -- -- Let new flights take-off from the runway. - -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Runway ) - -- - -- -- Let new flights take-off from the airbase hot. - -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Hot ) - -- - -- -- Let new flights take-off from the airbase cold. - -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Cold ) - -- - -- - -- @return #AI_A2G_DISPATCHER - -- - function AI_A2G_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.Takeoff = Takeoff - - return self - end - - - --- Gets the default method at which new flights will spawn and take-off as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default take-off in the air. - -- local TakeoffMethod = A2GDispatcher:GetDefaultTakeoff() - -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then - -- ... - -- end - -- - function AI_A2G_DISPATCHER:GetDefaultTakeoff( ) - - return self.DefenderDefault.Takeoff - end - - --- Gets the method at which new flights will spawn and take-off as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off in the air. - -- local TakeoffMethod = A2GDispatcher:GetSquadronTakeoff( "SquadronName" ) - -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then - -- ... - -- end - -- - function AI_A2G_DISPATCHER:GetSquadronTakeoff( SquadronName ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff - end - - - --- Sets flights to default take-off in the air, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default take-off in the air. - -- A2GDispatcher:SetDefaultTakeoffInAir() - -- - -- @return #AI_A2G_DISPATCHER - -- - function AI_A2G_DISPATCHER:SetDefaultTakeoffInAir() - - self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Air ) - - return self - end - - - --- Sets flights to take-off in the air, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number TakeoffAltitude (optional) The altitude in meters above the ground. If not given, the default takeoff altitude will be used. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off in the air. - -- A2GDispatcher:SetSquadronTakeoffInAir( "SquadronName" ) - -- - -- @return #AI_A2G_DISPATCHER - -- - function AI_A2G_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude ) - - self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Air ) - - if TakeoffAltitude then - self:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) - end - - return self - end - - - --- Sets flights by default to take-off from the runway, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default take-off from the runway. - -- A2GDispatcher:SetDefaultTakeoffFromRunway() - -- - -- @return #AI_A2G_DISPATCHER - -- - function AI_A2G_DISPATCHER:SetDefaultTakeoffFromRunway() - - self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Runway ) - - return self - end - - - --- Sets flights to take-off from the runway, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off from the runway. - -- A2GDispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) - -- - -- @return #AI_A2G_DISPATCHER - -- - function AI_A2G_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName ) - - self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Runway ) - - return self - end - - - --- Sets flights by default to take-off from the airbase at a hot location, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default take-off at a hot parking spot. - -- A2GDispatcher:SetDefaultTakeoffFromParkingHot() - -- - -- @return #AI_A2G_DISPATCHER - -- - function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingHot() - - self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Hot ) - - return self - end - - --- Sets flights to take-off from the airbase at a hot location, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off in the air. - -- A2GDispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) - -- - -- @return #AI_A2G_DISPATCHER - -- - function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName ) - - self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Hot ) - - return self - end - - - --- Sets flights to by default take-off from the airbase at a cold location, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off from a cold parking spot. - -- A2GDispatcher:SetDefaultTakeoffFromParkingCold() - -- - -- @return #AI_A2G_DISPATCHER - -- - function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingCold() - - self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Cold ) - - return self - end - - - --- Sets flights to take-off from the airbase at a cold location, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights take-off from a cold parking spot. - -- A2GDispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) - -- - -- @return #AI_A2G_DISPATCHER - -- - function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName ) - - self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Cold ) - - return self - end - - - --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. - -- @param #AI_A2G_DISPATCHER self - -- @param #number TakeoffAltitude The altitude in meters above the ground. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Set the default takeoff altitude when taking off in the air. - -- A2GDispatcher:SetDefaultTakeoffInAirAltitude( 2000 ) -- This makes planes start at 2000 meters above the ground. - -- - -- @return #AI_A2G_DISPATCHER - -- - function AI_A2G_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude ) - - self.DefenderDefault.TakeoffAltitude = TakeoffAltitude - - return self - end - - --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number TakeoffAltitude The altitude in meters above the ground. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Set the default takeoff altitude when taking off in the air. - -- A2GDispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. - -- - -- @return #AI_A2G_DISPATCHER - -- - function AI_A2G_DISPATCHER:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.TakeoffAltitude = TakeoffAltitude - - return self - end - - - --- Defines the default method at which flights will land and despawn as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default despawn near the airbase when returning. - -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.NearAirbase ) - -- - -- -- Let new flights by default despawn after landing land at the runway. - -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtRunway ) - -- - -- -- Let new flights by default despawn after landing and parking, and after engine shutdown. - -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtEngineShutdown ) - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetDefaultLanding( Landing ) - - self.DefenderDefault.Landing = Landing - - return self - end - - - --- Defines the method at which flights will land and despawn as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights despawn near the airbase when returning. - -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.NearAirbase ) - -- - -- -- Let new flights despawn after landing land at the runway. - -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtRunway ) - -- - -- -- Let new flights despawn after landing and parking, and after engine shutdown. - -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtEngineShutdown ) - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronLanding( SquadronName, Landing ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.Landing = Landing - - return self - end - - - --- Gets the default method at which flights will land and despawn as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights by default despawn near the airbase when returning. - -- local LandingMethod = A2GDispatcher:GetDefaultLanding( AI_A2G_Dispatcher.Landing.NearAirbase ) - -- if LandingMethod == AI_A2G_Dispatcher.Landing.NearAirbase then - -- ... - -- end - -- - function AI_A2G_DISPATCHER:GetDefaultLanding() - - return self.DefenderDefault.Landing - end - - - --- Gets the method at which flights will land and despawn as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let new flights despawn near the airbase when returning. - -- local LandingMethod = A2GDispatcher:GetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.NearAirbase ) - -- if LandingMethod == AI_A2G_Dispatcher.Landing.NearAirbase then - -- ... - -- end - -- - function AI_A2G_DISPATCHER:GetSquadronLanding( SquadronName ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - return DefenderSquadron.Landing or self.DefenderDefault.Landing - end - - - --- Sets flights by default to land and despawn near the airbase in the air, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let flights by default to land near the airbase and despawn. - -- A2GDispatcher:SetDefaultLandingNearAirbase() - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetDefaultLandingNearAirbase() - - self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.NearAirbase ) - - return self - end - - - --- Sets flights to land and despawn near the airbase in the air, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let flights to land near the airbase and despawn. - -- A2GDispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) - - self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.NearAirbase ) - - return self - end - - - --- Sets flights by default to land and despawn at the runway, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let flights by default land at the runway and despawn. - -- A2GDispatcher:SetDefaultLandingAtRunway() - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetDefaultLandingAtRunway() - - self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.AtRunway ) - - return self - end - - - --- Sets flights to land and despawn at the runway, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let flights land at the runway and despawn. - -- A2GDispatcher:SetSquadronLandingAtRunway( "SquadronName" ) - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) - - self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.AtRunway ) - - return self - end - - - --- Sets flights by default to land and despawn at engine shutdown, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let flights by default land and despawn at engine shutdown. - -- A2GDispatcher:SetDefaultLandingAtEngineShutdown() - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetDefaultLandingAtEngineShutdown() - - self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.AtEngineShutdown ) - - return self - end - - - --- Sets flights to land and despawn at engine shutdown, as part of the defense system. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @usage: - -- - -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) - -- - -- -- Let flights land and despawn at engine shutdown. - -- A2GDispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) - -- - -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) - - self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.AtEngineShutdown ) - - return self - end - - --- Set the default fuel treshold when defenders will RTB or Refuel in the air. - -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. - -- @param #AI_A2G_DISPATCHER self - -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the default fuel treshold. - -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- - function AI_A2G_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) - - self.DefenderDefault.FuelThreshold = FuelThreshold - - return self - end - - - --- Set the fuel treshold for the squadron when defenders will RTB or Refuel in the air. - -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the default fuel treshold. - -- A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- - function AI_A2G_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.FuelThreshold = FuelThreshold - - return self - end - - --- Set the default tanker where defenders will Refuel in the air. - -- @param #AI_A2G_DISPATCHER self - -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the default fuel treshold. - -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- - -- -- Now Setup the default tanker. - -- A2GDispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. - function AI_A2G_DISPATCHER:SetDefaultTanker( TankerName ) - - self.DefenderDefault.TankerName = TankerName - - return self - end - - - --- Set the squadron tanker where defenders will Refuel in the air. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The name of the squadron. - -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. - -- @return #AI_A2G_DISPATCHER - -- @usage - -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) - -- - -- -- Now Setup the squadron fuel treshold. - -- A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- - -- -- Now Setup the squadron tanker. - -- A2GDispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. - function AI_A2G_DISPATCHER:SetSquadronTanker( SquadronName, TankerName ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.TankerName = TankerName - - return self - end - - - - - --- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size ) - self.Defenders = self.Defenders or {} - local DefenderName = Defender:GetName() - self.Defenders[ DefenderName ] = Squadron - if Squadron.ResourceCount then - Squadron.ResourceCount = Squadron.ResourceCount - Size - end - self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) - end - - --- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) - self.Defenders = self.Defenders or {} - local DefenderName = Defender:GetName() - if Squadron.ResourceCount then - Squadron.ResourceCount = Squadron.ResourceCount + Defender:GetSize() - end - self.Defenders[ DefenderName ] = nil - self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) - end - - function AI_A2G_DISPATCHER:GetSquadronFromDefender( Defender ) - self.Defenders = self.Defenders or {} - local DefenderName = Defender:GetName() - self:F( { DefenderName = DefenderName } ) - return self.Defenders[ DefenderName ] - end - - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:CountPatrolAirborne( SquadronName, DefenseTaskType ) - - local PatrolCount = 0 - - local DefenderSquadron = self.DefenderSquadrons[SquadronName] - if DefenderSquadron then - for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do - if DefenderTask.SquadronName == SquadronName then - if DefenderTask.Type == DefenseTaskType then - if AIGroup:IsAlive() then - -- Check if the Patrol is patrolling or engaging. If not, this is not a valid Patrol, even if it is alive! - -- The Patrol could be damaged, lost control, or out of fuel! - if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) - or DefenderTask.Fsm:Is( "Started" ) then - PatrolCount = PatrolCount + 1 - end - end - end - end - end - end - - return PatrolCount - end - - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:CountDefendersEngaged( AttackerDetection ) - - -- First, count the active AIGroups Units, targetting the DetectedSet - local DefendersEngaged = 0 - local DefendersTotal = 0 - - local AttackerSet = AttackerDetection.Set - local AttackerCount = AttackerSet:Count() - local DefendersMissing = AttackerCount - --DetectedSet:Flush() - - local DefenderTasks = self:GetDefenderTasks() - for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do - local Defender = DefenderGroup -- Wrapper.Group#GROUP - local DefenderTaskTarget = DefenderTask.Target - local DefenderSquadronName = DefenderTask.SquadronName - local DefenderSize = DefenderTask.Size - - -- Count the total of defenders on the battlefield. - --local DefenderSize = Defender:GetInitialSize() - if DefenderTask.Target then - --if DefenderTask.Fsm:Is( "Engaging" ) then - self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) - DefendersTotal = DefendersTotal + DefenderSize - if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then - - local SquadronOverhead = self:GetSquadronOverhead( DefenderSquadronName ) - if DefenderSize then - DefendersEngaged = DefendersEngaged + DefenderSize - DefendersMissing = DefendersMissing - DefenderSize / SquadronOverhead - self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) - else - DefendersEngaged = 0 - end - end - --end - end - - - end - - self:F( { DefenderCount = DefendersEngaged } ) - - return DefendersTotal, DefendersEngaged, DefendersMissing - end - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:CountDefenders( AttackerDetection, DefenderCount, DefenderTaskType ) - - local Friendlies = nil - - local AttackerSet = AttackerDetection.Set - local AttackerCount = AttackerSet:Count() - - local DefenderFriendlies = self:GetDefenderFriendliesNearBy( AttackerDetection ) - - for FriendlyDistance, DefenderFriendlyUnit in UTILS.spairs( DefenderFriendlies or {} ) do - -- We only allow to engage targets as long as the units on both sides are balanced. - if AttackerCount > DefenderCount then - local FriendlyGroup = DefenderFriendlyUnit:GetGroup() -- Wrapper.Group#GROUP - if FriendlyGroup and FriendlyGroup:IsAlive() then - -- Ok, so we have a friendly near the potential target. - -- Now we need to check if the AIGroup has a Task. - local DefenderTask = self:GetDefenderTask( FriendlyGroup ) - if DefenderTask then - -- The Task should be of the same type. - if DefenderTaskType == DefenderTask.Type then - -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet - if DefenderTask.Target == nil then - if DefenderTask.Fsm:Is( "Returning" ) - or DefenderTask.Fsm:Is( "Patrolling" ) then - Friendlies = Friendlies or {} - Friendlies[FriendlyGroup] = FriendlyGroup - DefenderCount = DefenderCount + FriendlyGroup:GetSize() - self:F( { Friendly = FriendlyGroup:GetName(), FriendlyDistance = FriendlyDistance } ) - end - end - end - end - end - else - break - end - end - - return Friendlies - end - - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded ) - - local SquadronName = DefenderSquadron.Name - DefendersNeeded = DefendersNeeded or 4 - local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping - DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded - - if self:IsSquadronVisible( SquadronName ) then - - -- Here we Patrol the new planes. - -- The Resources table is filled in advance. - local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template. - - -- We determine the grouping based on the parameters set. - self:F( { DefenderGrouping = DefenderGrouping } ) - - -- New we will form the group to spawn in. - -- We search for the first free resource matching the template. - local DefenderUnitIndex = 1 - local DefenderPatrolTemplate = nil - local DefenderName = nil - for GroupName, DefenderGroup in pairs( DefenderSquadron.Resources[TemplateID] or {} ) do - self:F( { GroupName = GroupName } ) - local DefenderTemplate = _DATABASE:GetGroupTemplate( GroupName ) - if DefenderUnitIndex == 1 then - DefenderPatrolTemplate = UTILS.DeepCopy( DefenderTemplate ) - self.DefenderPatrolIndex = self.DefenderPatrolIndex + 1 - DefenderPatrolTemplate.name = SquadronName .. "#" .. self.DefenderPatrolIndex .. "#" .. GroupName - DefenderName = DefenderPatrolTemplate.name - else - -- Add the unit in the template to the DefenderPatrolTemplate. - local DefenderUnitTemplate = DefenderTemplate.units[1] - DefenderPatrolTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate - end - DefenderUnitIndex = DefenderUnitIndex + 1 - DefenderSquadron.Resources[TemplateID][GroupName] = nil - if DefenderUnitIndex > DefenderGrouping then - break - end - - end - - if DefenderPatrolTemplate then - local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) - local SpawnGroup = GROUP:Register( DefenderName ) - DefenderPatrolTemplate.lateActivation = nil - DefenderPatrolTemplate.uncontrolled = nil - local Takeoff = self:GetSquadronTakeoff( SquadronName ) - DefenderPatrolTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type - DefenderPatrolTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action - local Defender = _DATABASE:Spawn( DefenderPatrolTemplate ) - - self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) - return Defender, DefenderGrouping - end - else - local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN - if DefenderGrouping then - Spawn:InitGrouping( DefenderGrouping ) - else - Spawn:InitGrouping() - end - - local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) - local Defender = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP - self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) - return Defender, DefenderGrouping - end - - return nil, nil - end - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:onafterPatrol( From, Event, To, SquadronName, DefenseTaskType ) - - self:F({SquadronName = SquadronName}) - - local DefenderSquadron, Patrol = self:CanPatrol( SquadronName, DefenseTaskType ) - - if Patrol then - - local DefenderPatrol, DefenderGrouping = self:ResourceActivate( DefenderSquadron ) - - if DefenderPatrol then - - local Fsm = AI_A2G_PATROL:New( DefenderPatrol, Patrol.Zone, Patrol.FloorAltitude, Patrol.CeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.AltType ) - Fsm:SetDispatcher( self ) - Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) - Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) - Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) - Fsm:SetDisengageRadius( self.DisengageRadius ) - Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) - Fsm:Start() - - self:SetDefenderTask( SquadronName, DefenderPatrol, DefenseTaskType, Fsm ) - - function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"Patrol Birth", Defender:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - - if Squadron then - Fsm:__Patrol( 2 ) -- Start Patrolling - end - end - - function Fsm:onafterRTB( Defender, From, Event, To ) - self:F({"Patrol RTB", Defender:GetName()}) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) - local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - Dispatcher:ClearDefenderTaskTarget( Defender ) - end - - --- @param #AI_A2G_DISPATCHER self - function Fsm:onafterHome( Defender, From, Event, To, Action ) - self:F({"Patrol Home", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - - local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - - if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - - if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - self:ParkDefender( Squadron, Defender ) - end - end - end - end - - end - - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:onafterEngage( From, Event, To, AttackerDetection, Defenders ) - - if Defenders then - - for DefenderID, Defender in pairs( Defenders or {} ) do - - local Fsm = self:GetDefenderTaskFsm( Defender ) - Fsm:__Engage( 1, AttackerDetection.Set ) -- Engage on the TargetSetUnit - - self:SetDefenderTaskTarget( Defender, AttackerDetection ) - - end - end - end - - --- - -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:onafterDefend( From, Event, To, AttackerDetection, DefendersTotal, DefendersEngaged, DefendersMissing, DefenderFriendlies, DefenseTaskType ) - - self:F( { From, Event, To, AttackerDetection.Index, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing, DefenderFriendlies = DefenderFriendlies } ) - - local AttackerSet = AttackerDetection.Set - local AttackerUnit = AttackerSet:GetFirst() - - if AttackerUnit and AttackerUnit:IsAlive() then - local AttackerCount = AttackerSet:Count() - local DefenderCount = 0 - - for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do - - local SquadronName = self:GetDefenderTask( DefenderGroup ).SquadronName - local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) - - local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) - Fsm:__Engage( 1, AttackerSet ) -- Engage on the TargetSetUnit - - self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) - - local DefenderGroupSize = DefenderGroup:GetSize() - DefendersMissing = DefendersMissing - DefenderGroupSize / SquadronOverhead - DefendersTotal = DefendersTotal + DefenderGroupSize / SquadronOverhead - - if DefendersMissing <= 0 then - break - end - end - - self:F( { DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) - DefenderCount = DefendersMissing - - local ClosestDistance = 0 - local ClosestDefenderSquadronName = nil - - local BreakLoop = false - - while( DefenderCount > 0 and not BreakLoop ) do - - self:F( { DefenderSquadrons = self.DefenderSquadrons } ) - - for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons or {} ) do - - if DefenderSquadron[DefenseTaskType] then - - local SpawnCoord = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE - local AttackerCoord = AttackerUnit:GetCoordinate() - local InterceptCoord = AttackerDetection.InterceptCoord - self:F( { InterceptCoord = InterceptCoord } ) - if InterceptCoord then - local InterceptDistance = SpawnCoord:Get2DDistance( InterceptCoord ) - local AirbaseDistance = SpawnCoord:Get2DDistance( AttackerCoord ) - self:F( { InterceptDistance = InterceptDistance, AirbaseDistance = AirbaseDistance, InterceptCoord = InterceptCoord } ) - - if ClosestDistance == 0 or InterceptDistance < ClosestDistance then - - -- Only intercept if the distance to target is smaller or equal to the GciRadius limit. - if AirbaseDistance <= self.DefenseRadius then - ClosestDistance = InterceptDistance - ClosestDefenderSquadronName = SquadronName - end - end - end - end - end - - if ClosestDefenderSquadronName then - - local DefenderSquadron, Defense = self:CanDefend( ClosestDefenderSquadronName, DefenseTaskType ) - - if Defense then - - local DefenderOverhead = DefenderSquadron.Overhead or self.DefenderDefault.Overhead - local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping - local DefendersNeeded = math.ceil( DefenderCount * DefenderOverhead ) - - self:F( { Overhead = DefenderOverhead, SquadronOverhead = DefenderSquadron.Overhead , DefaultOverhead = self.DefenderDefault.Overhead } ) - self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } ) - self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } ) - - -- Validate that the maximum limit of Defenders has been reached. - -- If yes, then cancel the engaging of more defenders. - local DefendersLimit = DefenderSquadron.EngageLimit or self.DefenderDefault.EngageLimit - if DefendersLimit then - if DefendersTotal >= DefendersLimit then - DefendersNeeded = 0 - BreakLoop = true - else - -- If the total of amount of defenders + the defenders needed, is larger than the limit of defenders, - -- then the defenders needed is the difference between defenders total - defenders limit. - if DefendersTotal + DefendersNeeded > DefendersLimit then - DefendersNeeded = DefendersLimit - DefendersTotal - end - end - end - - -- DefenderSquadron.ResourceCount can have the value nil, which expresses unlimited resources. - -- DefendersNeeded cannot exceed DefenderSquadron.ResourceCount! - if DefenderSquadron.ResourceCount and DefendersNeeded > DefenderSquadron.ResourceCount then - DefendersNeeded = DefenderSquadron.ResourceCount - BreakLoop = true - end - - while ( DefendersNeeded > 0 ) do - - local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) - - DefendersNeeded = DefendersNeeded - DefenderGrouping - - if DefenderGroup then - - DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead - - local Fsm = AI_A2G_ENGAGE:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed ) - Fsm:SetDispatcher( self ) - Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) - Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) - Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) - Fsm:SetDisengageRadius( self.DisengageRadius ) - Fsm:Start() - - self:SetDefenderTask( ClosestDefenderSquadronName, DefenderGroup, DefenseTaskType, Fsm, AttackerDetection, DefenderGrouping ) - - function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"Defender Birth", Defender:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) - - if DefenderTarget then - Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit - end - end - - function Fsm:onafterRTB( Defender, From, Event, To ) - self:F({"Defender RTB", Defender:GetName()}) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) - - local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - Dispatcher:ClearDefenderTaskTarget( Defender ) - end - - --- @param #AI_A2G_DISPATCHER self - function Fsm:onafterLostControl( Defender, From, Event, To ) - self:F({"Defender LostControl", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - if Defender:IsAboveRunway() then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - end - - --- @param #AI_A2G_DISPATCHER self - function Fsm:onafterHome( Defender, From, Event, To, Action ) - self:F({"Defender Home", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - - local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - - if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - - if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - self:ParkDefender( Squadron, Defender ) - end - end - end -- if DefenderGCI then - end -- while ( DefendersNeeded > 0 ) do - else - -- No more resources, try something else. - -- Subject for a later enhancement to try to depart from another squadron and disable this one. - BreakLoop = true - break - end - else - -- There isn't any closest airbase anymore, break the loop. - break - end - end -- if DefenderSquadron then - end -- if AttackerUnit - end - - - - --- Creates an SEAD task when the targets have radars. - -- @param #AI_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. - -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. - -- @return #nil If there are no targets to be set. - function AI_A2G_DISPATCHER:Evaluate_SEAD( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT - local AttackerCount = AttackerSet:Count() - local IsSEAD = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group? - - if ( IsSEAD > 0 ) then - - -- First, count the active defenders, engaging the DetectedItem. - local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) - - self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) - - local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "SEAD" ) - - if DetectedItem.IsDetected == true then - - return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups - end - end - - return nil, nil, nil - end - - - --- Creates an CAS task. - -- @param #AI_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. - -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. - -- @return #nil If there are no targets to be set. - function AI_A2G_DISPATCHER:Evaluate_CAS( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT - local AttackerCount = AttackerSet:Count() - local AttackerRadarCount = AttackerSet:HasSEAD() - local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) - local IsCas = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == true ) -- Is the AttackerSet a CAS group? - - self:F( { Friendlies = self.Detection:GetFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) } ) - - if IsCas == true then - - -- First, count the active defenders, engaging the DetectedItem. - local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) - - self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) - - local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "CAS" ) - - if DetectedItem.IsDetected == true then - - return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups - end - end - - return nil, nil, nil - end - - - --- Evaluates an BAI task. - -- @param #AI_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. - -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. - -- @return #nil If there are no targets to be set. - function AI_A2G_DISPATCHER:Evaluate_BAI( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT - local AttackerCount = AttackerSet:Count() - local AttackerRadarCount = AttackerSet:HasSEAD() - local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) - local IsBai = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == false ) -- Is the AttackerSet a BAI group? - - if IsBai == true then - - -- First, count the active defenders, engaging the DetectedItem. - local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) - - self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) - - local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "BAI" ) - - if DetectedItem.IsDetected == true then - - return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups - end - end - - return nil, nil, nil - end - - - --- Assigns A2G AI Tasks in relation to the detected items. - -- @param #AI_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. - -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. - function AI_A2G_DISPATCHER:ProcessDetected( Detection ) - - local AreaMsg = {} - local TaskMsg = {} - local ChangeMsg = {} - - local TaskReport = REPORT:New() - - - for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do - local DefenderGroup = DefenderGroup -- Wrapper.Group#GROUP - if not DefenderGroup:IsAlive() then - local DefenderTaskFsm = self:GetDefenderTaskFsm( DefenderGroup ) - self:F( { Defender = DefenderGroup:GetName(), DefenderState = DefenderTaskFsm:GetState() } ) - if not DefenderTaskFsm:Is( "Started" ) then - self:ClearDefenderTask( DefenderGroup ) - end - else - if DefenderTask.Target then - local AttackerItem = Detection:GetDetectedItemByIndex( DefenderTask.Target.Index ) - if not AttackerItem then - self:F( { "Removing obsolete Target:", DefenderTask.Target.Index } ) - self:ClearDefenderTaskTarget( DefenderGroup ) - else - if DefenderTask.Target.Set then - local AttackerCount = DefenderTask.Target.Set:Count() - if AttackerCount == 0 then - self:F( { "All Targets destroyed in Target, removing:", DefenderTask.Target.Index } ) - self:ClearDefenderTaskTarget( DefenderGroup ) - end - end - end - end - end - end - - local Report = REPORT:New( "\nTactical Overview" ) - - local DefenderGroupCount = 0 - local Delay = 0 -- We need to implement a delay for each action because the spawning on airbases get confused if done too quick. - - local DefendersTotal = 0 - - -- Now that all obsolete tasks are removed, loop through the detected targets. - for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - - local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT - local DetectedCount = DetectedSet:Count() - local DetectedZone = DetectedItem.Zone - - self:F( { "Target ID", DetectedItem.ItemID } ) - DetectedSet:Flush( self ) - - local DetectedID = DetectedItem.ID - local DetectionIndex = DetectedItem.Index - local DetectedItemChanged = DetectedItem.Changed - - local AttackerCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) - - -- Calculate if for this DetectedItem if a defense needs to be initiated. - -- This calculation is based on the distance between the defense point and the attackers, and the defensiveness parameter. - -- The attackers closest to the defense coordinates will be handled first, or course! - - local DefenseCoordinate = nil - - for DefenseCoordinateName, EvaluateCoordinate in pairs( self.DefenseCoordinates ) do - - local EvaluateDistance = AttackerCoordinate:Get2DDistance( EvaluateCoordinate ) - - if EvaluateDistance <= self.DefenseRadius then - - local DistanceProbability = ( self.DefenseRadius / EvaluateDistance * self.DefenseReactivity ) - local DefenseProbability = math.random() - - self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) - - if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then - DefenseCoordinate = EvaluateCoordinate - break - end - end - end - - if DefenseCoordinate then - do - local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_SEAD( DetectedItem ) -- Returns a SET_UNIT with the SEAD targets to be engaged... - if DefendersMissing and DefendersMissing > 0 then - self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) - self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "SEAD", DefenseCoordinate ) - Delay = Delay + 1 - end - end - - do - local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... - if DefendersMissing and DefendersMissing > 0 then - self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) - self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "CAS", DefenseCoordinate ) - Delay = Delay + 1 - end - end - - do - local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_BAI( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... - if DefendersMissing and DefendersMissing > 0 then - self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) - self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "BAI", DefenseCoordinate ) - Delay = Delay + 1 - end - end - end - --- do --- local DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) --- if DefendersMissing and DefendersMissing > 0 then --- self:F( { DefendersMissing = DefendersMissing } ) --- self:CAS( DetectedItem, DefendersMissing, Friendlies ) --- end --- end - - if self.TacticalDisplay then - -- Show tactical situation - Report:Add( string.format( "\n - Target %s ( %s ): ( #%d ) %s" , DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) ) - for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do - local Defender = Defender -- Wrapper.Group#GROUP - if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then - if Defender:IsAlive() then - DefenderGroupCount = DefenderGroupCount + 1 - local Fuel = Defender:GetFuelMin() * 100 - local Damage = Defender:GetLife() / Defender:GetLife0() * 100 - Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", - Defender:GetName(), - DefenderTask.Type, - DefenderTask.Fsm:GetState(), - Defender:GetSize(), - Fuel, - Damage, - Defender:HasTask() == true and "Executing" or "Idle" ) ) - end - end - end - end - end - - if self.TacticalDisplay then - Report:Add( "\n - No Targets:") - local TaskCount = 0 - for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do - TaskCount = TaskCount + 1 - local Defender = Defender -- Wrapper.Group#GROUP - if not DefenderTask.Target then - if Defender:IsAlive() then - local DefenderHasTask = Defender:HasTask() - local Fuel = Defender:GetFuelMin() * 100 - local Damage = Defender:GetLife() / Defender:GetLife0() * 100 - DefenderGroupCount = DefenderGroupCount + 1 - Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", - Defender:GetName(), - DefenderTask.Type, - DefenderTask.Fsm:GetState(), - Defender:GetSize(), - Fuel, - Damage, - Defender:HasTask() == true and "Executing" or "Idle" ) ) - end - end - end - Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) - - self:F( Report:Text( "\n" ) ) - trigger.action.outText( Report:Text( "\n" ), 25 ) - end - - return true - end - -end - -do - - --- Calculates which HUMAN friendlies are nearby the area. - -- @param #AI_A2G_DISPATCHER self - -- @param DetectedItem The detected item. - -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. - function AI_A2G_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) - - local DetectedSet = DetectedItem.Set - local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem ) - - local PlayerTypes = {} - local PlayersCount = 0 - - if PlayersNearBy then - local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() - for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do - local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT - local PlayerName = PlayerUnit:GetPlayerName() - --self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) - if PlayerUnit:IsAirPlane() and PlayerName ~= nil then - local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel() - PlayersCount = PlayersCount + 1 - local PlayerType = PlayerUnit:GetTypeName() - PlayerTypes[PlayerName] = PlayerType - if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then - end - end - end - - end - - --self:F( { PlayersCount = PlayersCount } ) - - local PlayerTypesReport = REPORT:New() - - if PlayersCount > 0 then - for PlayerName, PlayerType in pairs( PlayerTypes ) do - PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) ) - end - else - PlayerTypesReport:Add( "-" ) - end - - - return PlayersCount, PlayerTypesReport - end - - --- Calculates which friendlies are nearby the area. - -- @param #AI_A2G_DISPATCHER self - -- @param DetectedItem The detected item. - -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. - function AI_A2G_DISPATCHER:GetFriendliesNearBy( DetectedItem ) - - local DetectedSet = DetectedItem.Set - local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem ) - - local FriendlyTypes = {} - local FriendliesCount = 0 - - if FriendlyUnitsNearBy then - local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() - for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do - local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT - if FriendlyUnit:IsAirPlane() then - local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel() - FriendliesCount = FriendliesCount + 1 - local FriendlyType = FriendlyUnit:GetTypeName() - FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1 - if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then - end - end - end - - end - - --self:F( { FriendliesCount = FriendliesCount } ) - - local FriendlyTypesReport = REPORT:New() - - if FriendliesCount > 0 then - for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do - FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) ) - end - else - FriendlyTypesReport:Add( "-" ) - end - - - return FriendliesCount, FriendlyTypesReport - end - - --- Schedules a new Patrol for the given SquadronName. - -- @param #AI_A2G_DISPATCHER self - -- @param #string SquadronName The squadron name. - function AI_A2G_DISPATCHER:SchedulerPatrol( SquadronName ) - local PatrolTaskTypes = { "SEAD", "CAS", "BAI" } - local PatrolTaskType = PatrolTaskTypes[math.random(1,3)] - self:Patrol( SquadronName, PatrolTaskType ) - end - -end - -do - - --- @type AI_A2G_GCICAP - -- @extends #AI_A2G_DISPATCHER - - --- Create an automatic air defence system for a coalition setting up GCI and CAP air defenses. - -- The class derives from @{#AI_A2G_DISPATCHER} and thus, all the methods that are defined in the @{#AI_A2G_DISPATCHER} class, can be used also in AI\_A2G\_GCICAP. - -- - -- === - -- - -- # Demo Missions - -- - -- ### [AI\_A2G\_GCICAP for Caucasus](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-200%20-%20AI_A2G%20-%20GCICAP%20Demonstration) - -- ### [AI\_A2G\_GCICAP for NTTR](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-210%20-%20NTTR%20AI_A2G_GCICAP%20Demonstration) - -- ### [AI\_A2G\_GCICAP for Normandy](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-220%20-%20NORMANDY%20AI_A2G_GCICAP%20Demonstration) - -- - -- ### [AI\_A2G\_GCICAP for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching) - -- - -- === - -- - -- # YouTube Channel - -- - -- ### [DCS WORLD - MOOSE - A2G GCICAP - Build an automatic A2G Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) - -- - -- === - -- - -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia3.JPG) - -- - -- AI\_A2G\_GCICAP includes automatic spawning of Combat Air Patrol aircraft (CAP) and Ground Controlled Intercept aircraft (GCI) in response to enemy - -- air movements that are detected by an airborne or ground based radar network. - -- - -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system. - -- - -- The AI_A2G_GCICAP provides a lightweight configuration method using the mission editor. Within a very short time, and with very little coding, - -- the mission designer is able to configure a complete A2G defense system for a coalition using the DCS Mission Editor available functions. - -- Using the DCS Mission Editor, you define borders of the coalition which are guarded by GCICAP, - -- configure airbases to belong to the coalition, define squadrons flying certain types of planes or payloads per airbase, and define CAP zones. - -- **Very little lua needs to be applied, a one liner**, which is fully explained below, which can be embedded - -- right in a DO SCRIPT trigger action or in a larger DO SCRIPT FILE trigger action. - -- - -- CAP flights will take off and proceed to designated CAP zones where they will remain on station until the ground radars direct them to intercept - -- detected enemy aircraft or they run short of fuel and must return to base (RTB). - -- - -- When a CAP flight leaves their zone to perform a GCI or return to base a new CAP flight will spawn to take its place. - -- If all CAP flights are engaged or RTB then additional GCI interceptors will scramble to intercept unengaged enemy aircraft under ground radar control. - -- - -- In short it is a plug in very flexible and configurable air defence module for DCS World. - -- - -- === - -- - -- # The following actions need to be followed when using AI\_A2G\_GCICAP in your mission: - -- - -- ## 1) Configure a working AI\_A2G\_GCICAP defense system for ONE coalition. - -- - -- ### 1.1) Define which airbases are for which coalition. - -- - -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_1.JPG) - -- - -- Color the airbases red or blue. You can do this by selecting the airbase on the map, and select the coalition blue or red. - -- - -- ### 1.2) Place groups of units given a name starting with a **EWR prefix** of your choice to build your EWR network. - -- - -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_2.JPG) - -- - -- **All EWR groups starting with the EWR prefix (text) will be included in the detection system.** - -- - -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy. - -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. - -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). - -- Additionally, ANY other radar capable unit can be part of the EWR network! - -- Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. - -- The position of these units is very important as they need to provide enough coverage - -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. - -- - -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. - -- For example if they are a long way forward and can detect enemy planes on the ground and taking off - -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. - -- Having the radars further back will mean a slower escalation because fewer targets will be detected and - -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. - -- It all depends on what the desired effect is. - -- - -- EWR networks are **dynamically maintained**. By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, - -- increasing or decreasing the radar coverage of the Early Warning System. - -- - -- ### 1.3) Place Airplane or Helicopter Groups with late activation switched on above the airbases to define Squadrons. - -- - -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_3.JPG) - -- - -- These are **templates**, with a given name starting with a **Template prefix** above each airbase that you wanna have a squadron. - -- These **templates** need to be within 1.5km from the airbase center. They don't need to have a slot at the airplane, they can just be positioned above the airbase, - -- without a route, and should only have ONE unit. - -- - -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_4.JPG) - -- - -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** - -- - -- ### 1.4) Place floating helicopters to create the CAP zones defined by its route points. - -- - -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_5.JPG) - -- - -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** - -- - -- The helicopter indicates the start of the CAP zone. - -- The route points define the form of the CAP zone polygon. - -- - -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_6.JPG) - -- - -- **The place of the helicopter is important, as the airbase closest to the helicopter will be the airbase from where the CAP planes will take off for CAP.** - -- - -- ## 2) There are a lot of defaults set, which can be further modified using the methods in @{#AI_A2G_DISPATCHER}: - -- - -- ### 2.1) Planes are taking off in the air from the airbases. - -- - -- This prevents airbases to get cluttered with airplanes taking off, it also reduces the risk of human players colliding with taxiiing airplanes, - -- resulting in the airbase to halt operations. - -- - -- You can change the way how planes take off by using the inherited methods from AI\_A2G\_DISPATCHER: - -- - -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. - -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. - -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. - -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. - -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. - -- - -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. - -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: - -- - -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. - -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. - -- * aircraft may collide at the airbase. - -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... - -- - -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. - -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! - -- - -- ### 2.2) Planes return near the airbase or will land if damaged. - -- - -- When damaged airplanes return to the airbase, they will be routed and will dissapear in the air when they are near the airbase. - -- There are exceptions to this rule, airplanes that aren't "listening" anymore due to damage or out of fuel, will return to the airbase and land. - -- - -- You can change the way how planes land by using the inherited methods from AI\_A2G\_DISPATCHER: - -- - -- * @{#AI_A2G_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. - -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. - -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. - -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. - -- - -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. - -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the - -- A2G defense system, as no new CAP or GCI planes can takeoff. - -- Note that the method @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. - -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. - -- - -- ### 2.3) CAP operations setup for specific airbases, will be executed with the following parameters: - -- - -- * The altitude will range between 6000 and 10000 meters. - -- * The CAP speed will vary between 500 and 800 km/h. - -- * The engage speed between 800 and 1200 km/h. - -- - -- You can change or add a CAP zone by using the inherited methods from AI\_A2G\_DISPATCHER: - -- - -- The method @{#AI_A2G_DISPATCHER.SetSquadronPatrol}() defines a CAP execution for a squadron. - -- - -- Setting-up a CAP zone also requires specific parameters: - -- - -- * The minimum and maximum altitude - -- * The minimum speed and maximum patrol speed - -- * The minimum and maximum engage speed - -- * The type of altitude measurement - -- - -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. - -- - -- The @{#AI_A2G_DISPATCHER.SetSquadronPatrolInterval}() method specifies **how much** and **when** CAP flights will takeoff. - -- - -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system. - -- - -- For example, the following setup will create a CAP for squadron "Sochi": - -- - -- A2GDispatcher:SetSquadronPatrol( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) - -- A2GDispatcher:SetSquadronPatrolInterval( "Sochi", 2, 30, 120, 1 ) - -- - -- ### 2.4) Each airbase will perform GCI when required, with the following parameters: - -- - -- * The engage speed is between 800 and 1200 km/h. - -- - -- You can change or add a GCI parameters by using the inherited methods from AI\_A2G\_DISPATCHER: - -- - -- The method @{#AI_A2G_DISPATCHER.SetSquadronGci}() defines a GCI execution for a squadron. - -- - -- Setting-up a GCI readiness also requires specific parameters: - -- - -- * The minimum speed and maximum patrol speed - -- - -- Essentially this controls how many flights of GCI aircraft can be active at any time. - -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers. - -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, - -- too short will mean that the intruders may have alraedy passed the ideal interception point! - -- - -- For example, the following setup will create a GCI for squadron "Sochi": - -- - -- A2GDispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) - -- - -- ### 2.5) Grouping or detected targets. - -- - -- Detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate - -- group being detected. - -- - -- Targets will be grouped within a radius of 30km by default. - -- - -- The radius indicates that detected targets need to be grouped within a radius of 30km. - -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. - -- Fast planes like in the 80s, need a larger radius than WWII planes. - -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft. - -- - -- ## 3) Additional notes: - -- - -- In order to create a two way A2G defense system, **two AI\_A2G\_GCICAP defense systems must need to be created**, for each coalition one. - -- Each defense system needs its own EWR network setup, airplane templates and CAP configurations. - -- - -- This is a good implementation, because maybe in the future, more coalitions may become available in DCS world. - -- - -- ## 4) Coding examples how to use the AI\_A2G\_GCICAP class: - -- - -- ### 4.1) An easy setup: - -- - -- -- Setup the AI_A2G_GCICAP dispatcher for one coalition, and initialize it. - -- GCI_Red = AI_A2G_GCICAP:New( "EWR CCCP", "SQUADRON CCCP", "CAP CCCP", 2 ) - -- -- - -- The following parameters were given to the :New method of AI_A2G_GCICAP, and mean the following: - -- - -- * `"EWR CCCP"`: Groups of the blue coalition are placed that define the EWR network. These groups start with the name `EWR CCCP`. - -- * `"SQUADRON CCCP"`: Late activated Groups objects of the red coalition are placed above the relevant airbases that will contain these templates in the squadron. - -- These late activated Groups start with the name `SQUADRON CCCP`. Each Group object contains only one Unit, and defines the weapon payload, skin and skill level. - -- * `"CAP CCCP"`: CAP Zones are defined using floating, late activated Helicopter Group objects, where the route points define the route of the polygon of the CAP Zone. - -- These Helicopter Group objects start with the name `CAP CCCP`, and will be the locations wherein CAP will be performed. - -- * `2` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. - -- - -- - -- ### 4.2) A more advanced setup: - -- - -- -- Setup the AI_A2G_GCICAP dispatcher for the blue coalition. - -- - -- A2G_GCICAP_Blue = AI_A2G_GCICAP:New( { "BLUE EWR" }, { "104th", "105th", "106th" }, { "104th CAP" }, 4 ) - -- - -- The following parameters for the :New method have the following meaning: - -- - -- * `{ "BLUE EWR" }`: An array of the group name prefixes of the groups of the blue coalition are placed that define the EWR network. These groups start with the name `BLUE EWR`. - -- * `{ "104th", "105th", "106th" } `: An array of the group name prefixes of the Late activated Groups objects of the blue coalition are - -- placed above the relevant airbases that will contain these templates in the squadron. - -- These late activated Groups start with the name `104th` or `105th` or `106th`. - -- * `{ "104th CAP" }`: An array of the names of the CAP zones are defined using floating, late activated helicopter group objects, - -- where the route points define the route of the polygon of the CAP Zone. - -- These Helicopter Group objects start with the name `104th CAP`, and will be the locations wherein CAP will be performed. - -- * `4` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. - -- - -- @field #AI_A2G_GCICAP - AI_A2G_GCICAP = { - ClassName = "AI_A2G_GCICAP", - Detection = nil, - } - - - --- AI_A2G_GCICAP constructor. - -- @param #AI_A2G_GCICAP self - -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. - -- @param #string TemplatePrefixes A list of template prefixes. - -- @param #string PatrolPrefixes A list of CAP zone prefixes (polygon zones). - -- @param #number PatrolLimit A number of how many CAP maximum will be spawned. - -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. - -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. - -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. - -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. - -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. - -- @return #AI_A2G_GCICAP - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, - -- -- will be considered a defense task if the target is within 60km from the defender. - -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. - -- -- The EWR network group prefix is DF CCCP. All groups starting with DF CCCP will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, - -- -- will be considered a defense task if the target is within 60km from the defender. - -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. - -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, - -- -- will be considered a defense task if the target is within 60km from the defender. - -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. - -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. - -- - -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is nil. No CAP is created. - -- -- The CAP Limit is nil. - -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. - -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. - -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. - -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. - -- - -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, nil, nil, nil, nil, nil, 30 ) - -- - function AI_A2G_GCICAP:New( EWRPrefixes, TemplatePrefixes, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) - - local EWRSetGroup = SET_GROUP:New() - EWRSetGroup:FilterPrefixes( EWRPrefixes ) - EWRSetGroup:FilterStart() - - local Detection = DETECTION_AREAS:New( EWRSetGroup, GroupingRadius or 30000 ) - - local self = BASE:Inherit( self, AI_A2G_DISPATCHER:New( Detection ) ) -- #AI_A2G_GCICAP - - self:SetGciRadius( GciRadius ) - - -- Determine the coalition of the EWRNetwork, this will be the coalition of the GCICAP. - local EWRFirst = EWRSetGroup:GetFirst() -- Wrapper.Group#GROUP - local EWRCoalition = EWRFirst:GetCoalition() - - -- Determine the airbases belonging to the coalition. - local AirbaseNames = {} -- #list<#string> - for AirbaseID, AirbaseData in pairs( _DATABASE.AIRBASES ) do - local Airbase = AirbaseData -- Wrapper.Airbase#AIRBASE - local AirbaseName = Airbase:GetName() - if Airbase:GetCoalition() == EWRCoalition then - table.insert( AirbaseNames, AirbaseName ) - end - end - - self.Templates = SET_GROUP - :New() - :FilterPrefixes( TemplatePrefixes ) - :FilterOnce() - - -- Setup squadrons - - self:I( { Airbases = AirbaseNames } ) - - self:I( "Defining Templates for Airbases ..." ) - for AirbaseID, AirbaseName in pairs( AirbaseNames ) do - local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE - local AirbaseName = Airbase:GetName() - local AirbaseCoord = Airbase:GetCoordinate() - local AirbaseZone = ZONE_RADIUS:New( "Airbase", AirbaseCoord:GetVec2(), 3000 ) - local Templates = nil - self:I( { Airbase = AirbaseName } ) - for TemplateID, Template in pairs( self.Templates:GetSet() ) do - local Template = Template -- Wrapper.Group#GROUP - local TemplateCoord = Template:GetCoordinate() - if AirbaseZone:IsVec2InZone( TemplateCoord:GetVec2() ) then - Templates = Templates or {} - table.insert( Templates, Template:GetName() ) - self:I( { Template = Template:GetName() } ) - end - end - if Templates then - self:SetSquadron( AirbaseName, AirbaseName, Templates, ResourceCount ) - end - end - - -- Setup CAP. - -- Find for each CAP the nearest airbase to the (start or center) of the zone. - -- CAP will be launched from there. - - self.CAPTemplates = SET_GROUP:New() - self.CAPTemplates:FilterPrefixes( PatrolPrefixes ) - self.CAPTemplates:FilterOnce() - - self:I( "Setting up CAP ..." ) - for CAPID, CAPTemplate in pairs( self.CAPTemplates:GetSet() ) do - local CAPZone = ZONE_POLYGON:New( CAPTemplate:GetName(), CAPTemplate ) - -- Now find the closest airbase from the ZONE (start or center) - local AirbaseDistance = 99999999 - local AirbaseClosest = nil -- Wrapper.Airbase#AIRBASE - self:I( { CAPZoneGroup = CAPID } ) - for AirbaseID, AirbaseName in pairs( AirbaseNames ) do - local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE - local AirbaseName = Airbase:GetName() - local AirbaseCoord = Airbase:GetCoordinate() - local Squadron = self.DefenderSquadrons[AirbaseName] - if Squadron then - local Distance = AirbaseCoord:Get2DDistance( CAPZone:GetCoordinate() ) - self:I( { AirbaseDistance = Distance } ) - if Distance < AirbaseDistance then - AirbaseDistance = Distance - AirbaseClosest = Airbase - end - end - end - if AirbaseClosest then - self:I( { CAPAirbase = AirbaseClosest:GetName() } ) - self:SetSquadronPatrol( AirbaseClosest:GetName(), CAPZone, 6000, 10000, 500, 800, 800, 1200, "RADIO" ) - self:SetSquadronPatrolInterval( AirbaseClosest:GetName(), PatrolLimit, 300, 600, 1 ) - end - end - - -- Setup GCI. - -- GCI is setup for all Squadrons. - self:I( "Setting up GCI ..." ) - for AirbaseID, AirbaseName in pairs( AirbaseNames ) do - local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE - local AirbaseName = Airbase:GetName() - local Squadron = self.DefenderSquadrons[AirbaseName] - self:F( { Airbase = AirbaseName } ) - if Squadron then - self:I( { GCIAirbase = AirbaseName } ) - self:SetSquadronGci( AirbaseName, 800, 1200 ) - end - end - - self:__Start( 5 ) - - self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) - self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) - --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) - - self:HandleEvent( EVENTS.Land ) - self:HandleEvent( EVENTS.EngineShutdown ) - - return self - end - - --- AI_A2G_GCICAP constructor with border. - -- @param #AI_A2G_GCICAP self - -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. - -- @param #string TemplatePrefixes A list of template prefixes. - -- @param #string BorderPrefix A Border Zone Prefix. - -- @param #string PatrolPrefixes A list of CAP zone prefixes (polygon zones). - -- @param #number PatrolLimit A number of how many CAP maximum will be spawned. - -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. - -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. - -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. - -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. - -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. - -- @return #AI_A2G_GCICAP - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- - -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- - -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, - -- -- will be considered a defense task if the target is within 60km from the defender. - -- - -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, - -- -- will be considered a defense task if the target is within 60km from the defender. - -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. - -- - -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. - -- -- The CAP Zone prefix is "CAP Zone". - -- -- The CAP Limit is 2. - -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, - -- -- will be considered a defense task if the target is within 60km from the defender. - -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. - -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. - -- - -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) - -- - -- @usage - -- - -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. - -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. - -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. - -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. - -- -- The CAP Zone prefix is nil. No CAP is created. - -- -- The CAP Limit is nil. - -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. - -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. - -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. - -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. - -- - -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", nil, nil, nil, nil, nil, 30 ) - -- - function AI_A2G_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) - - local self = AI_A2G_GCICAP:New( EWRPrefixes, TemplatePrefixes, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) - - if BorderPrefix then - self:SetBorderZone( ZONE_POLYGON:New( BorderPrefix, GROUP:FindByName( BorderPrefix ) ) ) - end - - return self - - end - -end - diff --git a/Moose Development/Moose/AI/AI_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua deleted file mode 100644 index 2cc6845b1..000000000 --- a/Moose Development/Moose/AI/AI_A2G_Engage.lua +++ /dev/null @@ -1,440 +0,0 @@ ---- **AI** -- Models the process of air to ground engagement for airplanes and helicopters. --- --- This is a class used in the @{AI_A2G_Dispatcher}. --- --- === --- --- ### Author: **FlightControl** --- --- === --- --- @module AI.AI_A2G_Engage --- @image AI_Air_To_Ground_Engage.JPG - - - ---- @type AI_A2G_ENGAGE --- @extends AI.AI_A2A#AI_A2A - - ---- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. --- --- ![Process](..\Presentations\AI_GCI\Dia3.JPG) --- --- The AI_A2G_ENGAGE is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_ENGAGE process can be started using the **Start** event. --- --- ![Process](..\Presentations\AI_GCI\Dia4.JPG) --- --- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- --- ![Process](..\Presentations\AI_GCI\Dia5.JPG) --- --- This cycle will continue. --- --- ![Process](..\Presentations\AI_GCI\Dia6.JPG) --- --- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. --- --- ![Process](..\Presentations\AI_GCI\Dia9.JPG) --- --- When enemies are detected, the AI will automatically engage the enemy. --- --- ![Process](..\Presentations\AI_GCI\Dia10.JPG) --- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Process](..\Presentations\AI_GCI\Dia13.JPG) --- --- ## 1. AI_A2G_ENGAGE constructor --- --- * @{#AI_A2G_ENGAGE.New}(): Creates a new AI_A2G_ENGAGE object. --- --- ## 3. Set the Range of Engagement --- --- ![Range](..\Presentations\AI_GCI\Dia11.JPG) --- --- An optional range can be set in meters, --- that will define when the AI will engage with the detected airborne enemy targets. --- The range can be beyond or smaller than the range of the Patrol Zone. --- The range is applied at the position of the AI. --- Use the method @{AI.AI_GCI#AI_A2G_ENGAGE.SetEngageRange}() to define that range. --- --- ## 4. Set the Zone of Engagement --- --- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) --- --- An optional @{Zone} can be set, --- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI.AI_Cap#AI_A2G_ENGAGE.SetEngageZone}() to define that Zone. --- --- === --- --- @field #AI_A2G_ENGAGE -AI_A2G_ENGAGE = { - ClassName = "AI_A2G_ENGAGE", -} - - - ---- Creates a new AI_A2G_ENGAGE object --- @param #AI_A2G_ENGAGE self --- @param Wrapper.Group#GROUP AIGroup --- @return #AI_A2G_ENGAGE -function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2G:New( AIGroup ) ) -- #AI_A2G_ENGAGE - - self.Accomplished = false - self.Engaging = false - - self.EngageMinSpeed = EngageMinSpeed - self.EngageMaxSpeed = EngageMaxSpeed - self.PatrolMinSpeed = EngageMinSpeed - self.PatrolMaxSpeed = EngageMaxSpeed - - self.PatrolAltType = "RADIO" - - self:AddTransition( { "Started", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_A2G_ENGAGE] OnBeforeEngage - -- @param #AI_A2G_ENGAGE self - -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_A2G_ENGAGE] OnAfterEngage - -- @param #AI_A2G_ENGAGE self - -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2G_ENGAGE] Engage - -- @param #AI_A2G_ENGAGE self - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2G_ENGAGE] __Engage - -- @param #AI_A2G_ENGAGE self - -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_A2G_ENGAGE] OnLeaveEngaging --- @param #AI_A2G_ENGAGE self --- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_A2G_ENGAGE] OnEnterEngaging --- @param #AI_A2G_ENGAGE self --- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_A2G_ENGAGE] OnBeforeFired - -- @param #AI_A2G_ENGAGE self - -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_A2G_ENGAGE] OnAfterFired - -- @param #AI_A2G_ENGAGE self - -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2G_ENGAGE] Fired - -- @param #AI_A2G_ENGAGE self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2G_ENGAGE] __Fired - -- @param #AI_A2G_ENGAGE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_A2G_ENGAGE] OnBeforeDestroy - -- @param #AI_A2G_ENGAGE self - -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_A2G_ENGAGE] OnAfterDestroy - -- @param #AI_A2G_ENGAGE self - -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2G_ENGAGE] Destroy - -- @param #AI_A2G_ENGAGE self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2G_ENGAGE] __Destroy - -- @param #AI_A2G_ENGAGE self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_A2G_ENGAGE] OnBeforeAbort - -- @param #AI_A2G_ENGAGE self - -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_A2G_ENGAGE] OnAfterAbort - -- @param #AI_A2G_ENGAGE self - -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2G_ENGAGE] Abort - -- @param #AI_A2G_ENGAGE self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2G_ENGAGE] __Abort - -- @param #AI_A2G_ENGAGE self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2G_ENGAGE] OnBeforeAccomplish - -- @param #AI_A2G_ENGAGE self - -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2G_ENGAGE] OnAfterAccomplish - -- @param #AI_A2G_ENGAGE self - -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2G_ENGAGE] Accomplish - -- @param #AI_A2G_ENGAGE self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2G_ENGAGE] __Accomplish - -- @param #AI_A2G_ENGAGE self - -- @param #number Delay The delay in seconds. - - return self -end - ---- onafter event handler for Start event. --- @param #AI_A2G_ENGAGE self --- @param Wrapper.Group#GROUP AIGroup The AI group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_ENGAGE:onafterStart( AIGroup, From, Event, To ) - - self:GetParent( self ).onafterStart( self, AIGroup, From, Event, To ) - AIGroup:HandleEvent( EVENTS.Takeoff, nil, self ) - -end - - - ---- onafter event handler for Engage event. --- @param #AI_A2G_ENGAGE self --- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_ENGAGE:onafterEngage( AIGroup, From, Event, To ) - - self:HandleEvent( EVENTS.Dead ) - -end - --- todo: need to fix this global function - ---- @param Wrapper.Group#GROUP AIControllable -function AI_A2G_ENGAGE.EngageRoute( AIGroup, Fsm ) - - AIGroup:F( { "AI_A2G_ENGAGE.EngageRoute:", AIGroup:GetName() } ) - - if AIGroup:IsAlive() then - Fsm:__Engage( 0.5 ) - - --local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) - --AIGroup:SetTask( Task ) - end -end - ---- onbefore event handler for Engage event. --- @param #AI_A2G_ENGAGE self --- @param Wrapper.Group#GROUP AIGroup The group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_ENGAGE:onbeforeEngage( AIGroup, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- onafter event handler for Abort event. --- @param #AI_A2G_ENGAGE self --- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_ENGAGE:onafterAbort( AIGroup, From, Event, To ) - AIGroup:ClearTasks() - self:Return() - self:__RTB( 0.5 ) -end - - ---- @param #AI_A2G_ENGAGE self --- @param Wrapper.Group#GROUP AIGroup The GroupGroup managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_ENGAGE:onafterEngage( AIGroup, From, Event, To, AttackSetUnit ) - - self:F( { AIGroup, From, Event, To, AttackSetUnit} ) - - self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT - - local FirstAttackUnit = self.AttackSetUnit:GetFirst() - - if FirstAttackUnit and FirstAttackUnit:IsAlive() then - - if AIGroup:IsAlive() then - - local EngageRoute = {} - - local CurrentCoord = AIGroup:GetCoordinate() - - --- Calculate the target route point. - - local CurrentCoord = AIGroup:GetCoordinate() - - local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - self:SetTargetDistance( ToTargetCoord ) -- For RTB status check - - local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - local ToEngageAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = CurrentCoord:Translate( 15000, ToEngageAngle ):WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = ToEngageAngle, ToTargetSpeed = ToTargetSpeed } ) - self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - - local AttackTasks = {} - - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:T( { "Eliminating Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) - AttackTasks[#AttackTasks+1] = AIGroup:TaskAttackUnit( AttackUnit ) - end - end - - if #AttackTasks == 0 then - self:E("No targets found -> Going RTB") - self:Return() - self:__RTB( 0.5 ) - else - AIGroup:OptionROEOpenFire() - AIGroup:OptionROTEvadeFire() - - AttackTasks[#AttackTasks+1] = AIGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) - EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( AttackTasks ) - end - - AIGroup:Route( EngageRoute, 0.5 ) - - end - else - self:E("No targets found -> Going RTB") - self:Return() - self:__RTB( 0.5 ) - end -end - ---- @param #AI_A2G_ENGAGE self --- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_ENGAGE:onafterAccomplish( AIGroup, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -end - ---- @param #AI_A2G_ENGAGE self --- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_A2G_ENGAGE:onafterDestroy( AIGroup, From, Event, To, EventData ) - - if EventData.IniUnit then - self.AttackUnits[EventData.IniUnit] = nil - end -end - ---- @param #AI_A2G_ENGAGE self --- @param Core.Event#EVENTDATA EventData -function AI_A2G_ENGAGE:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end -end diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua deleted file mode 100644 index bf4a97dba..000000000 --- a/Moose Development/Moose/AI/AI_A2G_Patrol.lua +++ /dev/null @@ -1,488 +0,0 @@ ---- **AI** -- Models the process of A2G patrolling and engaging ground targets for airplanes and helicopters. --- --- === --- --- ### Author: **FlightControl** --- --- === --- --- @module AI.AI_A2G_Patrol --- @image AI_Air_To_Ground_Patrol.JPG - ---- @type AI_A2G_PATROL --- @extends AI.AI_A2A_Patrol#AI_A2A_PATROL - - ---- The AI_A2G_PATROL class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} --- and automatically engage any airborne enemies that are within a certain range or within a certain zone. --- --- ![Process](..\Presentations\AI_CAP\Dia3.JPG) --- --- The AI_A2G_PATROL is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_PATROL process can be started using the **Start** event. --- --- ![Process](..\Presentations\AI_CAP\Dia4.JPG) --- --- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. --- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- --- ![Process](..\Presentations\AI_CAP\Dia5.JPG) --- --- This cycle will continue. --- --- ![Process](..\Presentations\AI_CAP\Dia6.JPG) --- --- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. --- --- ![Process](..\Presentations\AI_CAP\Dia9.JPG) --- --- When enemies are detected, the AI will automatically engage the enemy. --- --- ![Process](..\Presentations\AI_CAP\Dia10.JPG) --- --- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. --- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- --- ![Process](..\Presentations\AI_CAP\Dia13.JPG) --- --- ## 1. AI_A2G_PATROL constructor --- --- * @{#AI_A2G_PATROL.New}(): Creates a new AI_A2G_PATROL object. --- --- ## 2. AI_A2G_PATROL is a FSM --- --- ![Process](..\Presentations\AI_CAP\Dia2.JPG) --- --- ### 2.1 AI_A2G_PATROL States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Engaging** ( Group ): The AI is engaging the bogeys. --- * **Returning** ( Group ): The AI is returning to Base.. --- --- ### 2.2 AI_A2G_PATROL Events --- --- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. --- * **@{#AI_A2G_PATROL.Engage}**: Let the AI engage the bogeys. --- * **@{#AI_A2G_PATROL.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_A2G_PATROL.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. --- * **@{#AI_A2G_PATROL.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 3. Set the Range of Engagement --- --- ![Range](..\Presentations\AI_CAP\Dia11.JPG) --- --- An optional range can be set in meters, --- that will define when the AI will engage with the detected airborne enemy targets. --- The range can be beyond or smaller than the range of the Patrol Zone. --- The range is applied at the position of the AI. --- Use the method @{AI.AI_CAP#AI_A2G_PATROL.SetEngageRange}() to define that range. --- --- ## 4. Set the Zone of Engagement --- --- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) --- --- An optional @{Zone} can be set, --- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI.AI_Cap#AI_A2G_PATROL.SetEngageZone}() to define that Zone. --- --- === --- --- @field #AI_A2G_PATROL -AI_A2G_PATROL = { - ClassName = "AI_A2G_PATROL", -} - ---- Creates a new AI_A2G_PATROL object --- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol --- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. --- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. --- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. --- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. --- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO --- @return #AI_A2G_PATROL -function AI_A2G_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, PatrolAltType ) - - -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2A_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_PATROL - - self.Accomplished = false - self.Engaging = false - - self.EngageMinSpeed = EngageMinSpeed - self.EngageMaxSpeed = EngageMaxSpeed - - self:AddTransition( { "Patrolling", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_A2G_PATROL] OnBeforeEngage - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_A2G_PATROL] OnAfterEngage - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2G_PATROL] Engage - -- @param #AI_A2G_PATROL self - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2G_PATROL] __Engage - -- @param #AI_A2G_PATROL self - -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_A2G_PATROL] OnLeaveEngaging --- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_A2G_PATROL] OnEnterEngaging --- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_A2G_PATROL] OnBeforeFired - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_A2G_PATROL] OnAfterFired - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2G_PATROL] Fired - -- @param #AI_A2G_PATROL self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2G_PATROL] __Fired - -- @param #AI_A2G_PATROL self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_A2G_PATROL] OnBeforeDestroy - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_A2G_PATROL] OnAfterDestroy - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2G_PATROL] Destroy - -- @param #AI_A2G_PATROL self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2G_PATROL] __Destroy - -- @param #AI_A2G_PATROL self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_A2G_PATROL] OnBeforeAbort - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_A2G_PATROL] OnAfterAbort - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2G_PATROL] Abort - -- @param #AI_A2G_PATROL self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2G_PATROL] __Abort - -- @param #AI_A2G_PATROL self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2G_PATROL] OnBeforeAccomplish - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2G_PATROL] OnAfterAccomplish - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2G_PATROL] Accomplish - -- @param #AI_A2G_PATROL self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2G_PATROL] __Accomplish - -- @param #AI_A2G_PATROL self - -- @param #number Delay The delay in seconds. - - return self -end - - ---- onafter State Transition for Event Patrol. --- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_PATROL:onafterStart( AIPatrol, From, Event, To ) - - self:GetParent( self ).onafterStart( self, AIPatrol, From, Event, To ) - AIPatrol:HandleEvent( EVENTS.Takeoff, nil, self ) - -end - ---- Set the Engage Zone which defines where the AI will engage bogies. --- @param #AI_A2G_PATROL self --- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. --- @return #AI_A2G_PATROL self -function AI_A2G_PATROL:SetEngageZone( EngageZone ) - self:F2() - - if EngageZone then - self.EngageZone = EngageZone - else - self.EngageZone = nil - end -end - ---- Set the Engage Range when the AI will engage with airborne enemies. --- @param #AI_A2G_PATROL self --- @param #number EngageRange The Engage Range. --- @return #AI_A2G_PATROL self -function AI_A2G_PATROL:SetEngageRange( EngageRange ) - self:F2() - - if EngageRange then - self.EngageRange = EngageRange - else - self.EngageRange = nil - end -end - ---- onafter State Transition for Event Patrol. --- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_PATROL:onafterPatrol( AIPatrol, From, Event, To ) - - -- Call the parent Start event handler - self:GetParent(self).onafterPatrol( self, AIPatrol, From, Event, To ) - self:HandleEvent( EVENTS.Dead ) - -end - --- todo: need to fix this global function - ---- @param Wrapper.Group#GROUP AIPatrol -function AI_A2G_PATROL.AttackRoute( AIPatrol, Fsm ) - - AIPatrol:F( { "AI_A2G_PATROL.AttackRoute:", AIPatrol:GetName() } ) - - if AIPatrol:IsAlive() then - Fsm:__Engage( 0.5 ) - end -end - ---- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_PATROL:onbeforeEngage( AIPatrol, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_PATROL:onafterAbort( AIPatrol, From, Event, To ) - AIPatrol:ClearTasks() - self:__Route( 0.5 ) -end - - ---- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The AIPatrol Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_PATROL:onafterEngage( AIPatrol, From, Event, To, AttackSetUnit ) - - self:F( { AIPatrol, From, Event, To, AttackSetUnit} ) - - self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT - - local FirstAttackUnit = self.AttackSetUnit:GetFirst() -- Wrapper.Unit#UNIT - - if FirstAttackUnit and FirstAttackUnit:IsAlive() then -- If there is no attacker anymore, stop the engagement. - - if AIPatrol:IsAlive() then - - local EngageRoute = {} - - --- Calculate the target route point. - local CurrentCoord = AIPatrol:GetCoordinate() - local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - local ToInterceptAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = CurrentCoord:Translate( 5000, ToInterceptAngle ):WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = ToInterceptAngle, ToTargetSpeed = ToTargetSpeed } ) - self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - - local AttackTasks = {} - - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT - self:T( { "Attacking Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - AttackTasks[#AttackTasks+1] = AIPatrol:TaskAttackUnit( AttackUnit ) - end - end - - if #AttackTasks == 0 then - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 0.5 ) - else - AIPatrol:OptionROEOpenFire() - AIPatrol:OptionROTEvadeFire() - - AttackTasks[#AttackTasks+1] = AIPatrol:TaskFunction( "AI_A2G_PATROL.AttackRoute", self ) - EngageRoute[#EngageRoute].task = AIPatrol:TaskCombo( AttackTasks ) - end - - AIPatrol:Route( EngageRoute, 0.5 ) - end - else - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 0.5 ) - end -end - ---- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_PATROL:onafterAccomplish( AIPatrol, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -end - ---- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_A2G_PATROL:onafterDestroy( AIPatrol, From, Event, To, EventData ) - - if EventData.IniUnit then - self.AttackUnits[EventData.IniUnit] = nil - end -end - ---- @param #AI_A2G_PATROL self --- @param Core.Event#EVENTDATA EventData -function AI_A2G_PATROL:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end -end - ---- @param Wrapper.Group#GROUP AIPatrol -function AI_A2G_PATROL.Resume( AIPatrol, Fsm ) - - AIPatrol:I( { "AI_A2G_PATROL.Resume:", AIPatrol:GetName() } ) - if AIPatrol:IsAlive() then - Fsm:__Reset( 1 ) - Fsm:__Route( 5 ) - end - -end diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua deleted file mode 100644 index 80a58bf7f..000000000 --- a/Moose Development/Moose/AI/AI_Air.lua +++ /dev/null @@ -1,732 +0,0 @@ ---- **AI** -- Models the process of AI air operations. --- --- === --- --- ### Author: **FlightControl** --- --- === --- --- @module AI.AI_Air --- @image AI_Air_Operations.JPG - ---- @type AI_AIR --- @extends Core.Fsm#FSM_CONTROLLABLE - ---- The AI_AIR class implements the core functions to operate an AI @{Wrapper.Group}. --- --- --- # 1) AI_AIR constructor --- --- * @{#AI_AIR.New}(): Creates a new AI_AIR object. --- --- # 2) AI_AIR is a Finite State Machine. --- --- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. --- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. --- --- So, each of the rows have the following structure. --- --- * **From** => **Event** => **To** --- --- Important to know is that an event can only be executed if the **current state** is the **From** state. --- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, --- and the resulting state will be the **To** state. --- --- These are the different possible state transitions of this state machine implementation: --- --- * Idle => Start => Monitoring --- --- ## 2.1) AI_AIR States. --- --- * **Idle**: The process is idle. --- --- ## 2.2) AI_AIR Events. --- --- * **Start**: Start the transport process. --- * **Stop**: Stop the transport process. --- * **Monitor**: Monitor and take action. --- --- @field #AI_AIR -AI_AIR = { - ClassName = "AI_AIR", -} - ---- Creates a new AI_AIR process. --- @param #AI_AIR self --- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process. --- @return #AI_AIR -function AI_AIR:New( AIGroup ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_AIR - - self:SetControllable( AIGroup ) - - self:SetStartState( "Stopped" ) - - self:AddTransition( "*", "Start", "Started" ) - - --- Start Handler OnBefore for AI_AIR - -- @function [parent=#AI_AIR] OnBeforeStart - -- @param #AI_AIR self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Start Handler OnAfter for AI_AIR - -- @function [parent=#AI_AIR] OnAfterStart - -- @param #AI_AIR self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Start Trigger for AI_AIR - -- @function [parent=#AI_AIR] Start - -- @param #AI_AIR self - - --- Start Asynchronous Trigger for AI_AIR - -- @function [parent=#AI_AIR] __Start - -- @param #AI_AIR self - -- @param #number Delay - - self:AddTransition( "*", "Stop", "Stopped" ) - ---- OnLeave Transition Handler for State Stopped. --- @function [parent=#AI_AIR] OnLeaveStopped --- @param #AI_AIR self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Stopped. --- @function [parent=#AI_AIR] OnEnterStopped --- @param #AI_AIR self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- OnBefore Transition Handler for Event Stop. --- @function [parent=#AI_AIR] OnBeforeStop --- @param #AI_AIR self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Stop. --- @function [parent=#AI_AIR] OnAfterStop --- @param #AI_AIR self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Stop. --- @function [parent=#AI_AIR] Stop --- @param #AI_AIR self - ---- Asynchronous Event Trigger for Event Stop. --- @function [parent=#AI_AIR] __Stop --- @param #AI_AIR self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. - ---- OnBefore Transition Handler for Event Status. --- @function [parent=#AI_AIR] OnBeforeStatus --- @param #AI_AIR self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event Status. --- @function [parent=#AI_AIR] OnAfterStatus --- @param #AI_AIR self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event Status. --- @function [parent=#AI_AIR] Status --- @param #AI_AIR self - ---- Asynchronous Event Trigger for Event Status. --- @function [parent=#AI_AIR] __Status --- @param #AI_AIR self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "RTB", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. - ---- OnBefore Transition Handler for Event RTB. --- @function [parent=#AI_AIR] OnBeforeRTB --- @param #AI_AIR self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnAfter Transition Handler for Event RTB. --- @function [parent=#AI_AIR] OnAfterRTB --- @param #AI_AIR self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - ---- Synchronous Event Trigger for Event RTB. --- @function [parent=#AI_AIR] RTB --- @param #AI_AIR self - ---- Asynchronous Event Trigger for Event RTB. --- @function [parent=#AI_AIR] __RTB --- @param #AI_AIR self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Returning. --- @function [parent=#AI_AIR] OnLeaveReturning --- @param #AI_AIR self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Returning. --- @function [parent=#AI_AIR] OnEnterReturning --- @param #AI_AIR self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Patrolling", "Refuel", "Refuelling" ) - - --- Refuel Handler OnBefore for AI_AIR - -- @function [parent=#AI_AIR] OnBeforeRefuel - -- @param #AI_AIR self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Refuel Handler OnAfter for AI_AIR - -- @function [parent=#AI_AIR] OnAfterRefuel - -- @param #AI_AIR self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Refuel Trigger for AI_AIR - -- @function [parent=#AI_AIR] Refuel - -- @param #AI_AIR self - - --- Refuel Asynchronous Trigger for AI_AIR - -- @function [parent=#AI_AIR] __Refuel - -- @param #AI_AIR self - -- @param #number Delay - - self:AddTransition( "*", "Takeoff", "Airborne" ) - self:AddTransition( "*", "Return", "Returning" ) - self:AddTransition( "*", "Hold", "Holding" ) - self:AddTransition( "*", "Home", "Home" ) - self:AddTransition( "*", "LostControl", "LostControl" ) - self:AddTransition( "*", "Fuel", "Fuel" ) - self:AddTransition( "*", "Damaged", "Damaged" ) - self:AddTransition( "*", "Eject", "*" ) - self:AddTransition( "*", "Crash", "Crashed" ) - self:AddTransition( "*", "PilotDead", "*" ) - - self.IdleCount = 0 - - return self -end - ---- @param Wrapper.Group#GROUP self --- @param Core.Event#EVENTDATA EventData -function GROUP:OnEventTakeoff( EventData, Fsm ) - Fsm:Takeoff() - self:UnHandleEvent( EVENTS.Takeoff ) -end - - - -function AI_AIR:SetDispatcher( Dispatcher ) - self.Dispatcher = Dispatcher -end - -function AI_AIR:GetDispatcher() - return self.Dispatcher -end - -function AI_AIR:SetTargetDistance( Coordinate ) - - local CurrentCoord = self.Controllable:GetCoordinate() - self.TargetDistance = CurrentCoord:Get2DDistance( Coordinate ) - - self.ClosestTargetDistance = ( not self.ClosestTargetDistance or self.ClosestTargetDistance > self.TargetDistance ) and self.TargetDistance or self.ClosestTargetDistance -end - - -function AI_AIR:ClearTargetDistance() - - self.TargetDistance = nil - self.ClosestTargetDistance = nil -end - - ---- Sets (modifies) the minimum and maximum speed of the patrol. --- @param #AI_AIR self --- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. --- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. --- @return #AI_AIR self -function AI_AIR:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) - self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) - - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed -end - - ---- Sets the floor and ceiling altitude of the patrol. --- @param #AI_AIR self --- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @return #AI_AIR self -function AI_AIR:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) - self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) - - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude -end - - ---- Sets the home airbase. --- @param #AI_AIR self --- @param Wrapper.Airbase#AIRBASE HomeAirbase --- @return #AI_AIR self -function AI_AIR:SetHomeAirbase( HomeAirbase ) - self:F2( { HomeAirbase } ) - - self.HomeAirbase = HomeAirbase -end - ---- Sets to refuel at the given tanker. --- @param #AI_AIR self --- @param Wrapper.Group#GROUP TankerName The group name of the tanker as defined within the Mission Editor or spawned. --- @return #AI_AIR self -function AI_AIR:SetTanker( TankerName ) - self:F2( { TankerName } ) - - self.TankerName = TankerName -end - - ---- Sets the disengage range, that when engaging a target beyond the specified range, the engagement will be cancelled and the plane will RTB. --- @param #AI_AIR self --- @param #number DisengageRadius The disengage range. --- @return #AI_AIR self -function AI_AIR:SetDisengageRadius( DisengageRadius ) - self:F2( { DisengageRadius } ) - - self.DisengageRadius = DisengageRadius -end - ---- Set the status checking off. --- @param #AI_AIR self --- @return #AI_AIR self -function AI_AIR:SetStatusOff() - self:F2() - - self.CheckStatus = false -end - - ---- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. --- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_AIR. --- Once the time is finished, the old AI will return to the base. --- @param #AI_AIR self --- @param #number FuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. --- @param #number OutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. --- @return #AI_AIR self -function AI_AIR:SetFuelThreshold( FuelThresholdPercentage, OutOfFuelOrbitTime ) - - self.FuelThresholdPercentage = FuelThresholdPercentage - self.OutOfFuelOrbitTime = OutOfFuelOrbitTime - - self.Controllable:OptionRTBBingoFuel( false ) - - return self -end - ---- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. --- However, damage cannot be foreseen early on. --- Therefore, when the damage treshold is reached, --- the AI will return immediately to the home base (RTB). --- Note that for groups, the average damage of the complete group will be calculated. --- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. --- @param #AI_AIR self --- @param #number PatrolDamageThreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. --- @return #AI_AIR self -function AI_AIR:SetDamageThreshold( PatrolDamageThreshold ) - - self.PatrolManageDamage = true - self.PatrolDamageThreshold = PatrolDamageThreshold - - return self -end - ---- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_AIR self --- @return #AI_AIR self --- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_AIR:onafterStart( Controllable, From, Event, To ) - - self:__Status( 10 ) -- Check status status every 30 seconds. - - self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) - self:HandleEvent( EVENTS.Crash, self.OnCrash ) - self:HandleEvent( EVENTS.Ejection, self.OnEjection ) - - Controllable:OptionROEHoldFire() - Controllable:OptionROTVertical() -end - - - ---- @param #AI_AIR self -function AI_AIR:onbeforeStatus() - - return self.CheckStatus -end - ---- @param #AI_AIR self -function AI_AIR:onafterStatus() - - if self.Controllable and self.Controllable:IsAlive() then - - local RTB = false - - local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) - - if not self:Is( "Holding" ) and not self:Is( "Returning" ) then - local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) - self:F({DistanceFromHomeBase=DistanceFromHomeBase}) - - if DistanceFromHomeBase > self.DisengageRadius then - self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) - self:Hold( 300 ) - RTB = false - end - end - --- I think this code is not requirement anymore after release 2.5. --- if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then --- if DistanceFromHomeBase < 5000 then --- self:E( self.Controllable:GetName() .. " is near the home base, RTB!" ) --- self:Home( "Destroy" ) --- end --- end - - - if not self:Is( "Fuel" ) and not self:Is( "Home" ) then - local Fuel = self.Controllable:GetFuelMin() - self:F({Fuel=Fuel, FuelThresholdPercentage=self.FuelThresholdPercentage}) - if Fuel < self.FuelThresholdPercentage then - if self.TankerName then - self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) - self:Refuel() - else - self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... RTB!" ) - local OldAIControllable = self.Controllable - - local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) - local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.OutOfFuelOrbitTime,nil ) ) - OldAIControllable:SetTask( TimedOrbitTask, 10 ) - - self:Fuel() - RTB = true - end - else - end - end - - -- TODO: Check GROUP damage function. - local Damage = self.Controllable:GetLife() - local InitialLife = self.Controllable:GetLife0() - self:F( { Damage = Damage, InitialLife = InitialLife, DamageThreshold = self.PatrolDamageThreshold } ) - if ( Damage / InitialLife ) < self.PatrolDamageThreshold then - self:E( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) - self:Damaged() - RTB = true - self:SetStatusOff() - end - - -- Check if planes went RTB and are out of control. - -- We only check if planes are out of control, when they are in duty. - if self.Controllable:HasTask() == false then - if not self:Is( "Started" ) and - not self:Is( "Stopped" ) and - not self:Is( "Fuel" ) and - not self:Is( "Damaged" ) and - not self:Is( "Home" ) then - if self.IdleCount >= 2 then - if Damage ~= InitialLife then - self:Damaged() - else - self:E( self.Controllable:GetName() .. " control lost! " ) - self:LostControl() - end - else - self.IdleCount = self.IdleCount + 1 - end - end - else - self.IdleCount = 0 - end - - if RTB == true then - self:__RTB( 0.5 ) - end - - if not self:Is("Home") then - self:__Status( 10 ) - end - - end -end - - ---- @param Wrapper.Group#GROUP AIGroup -function AI_AIR.RTBRoute( AIGroup, Fsm ) - - AIGroup:F( { "AI_AIR.RTBRoute:", AIGroup:GetName() } ) - - if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) - end - -end - ---- @param Wrapper.Group#GROUP AIGroup -function AI_AIR.RTBHold( AIGroup, Fsm ) - - AIGroup:F( { "AI_AIR.RTBHold:", AIGroup:GetName() } ) - if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) - Fsm:Return() - local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) - AIGroup:SetTask( Task ) - end - -end - - ---- @param #AI_AIR self --- @param Wrapper.Group#GROUP AIGroup -function AI_AIR:onafterRTB( AIGroup, From, Event, To ) - self:F( { AIGroup, From, Event, To } ) - - - if AIGroup and AIGroup:IsAlive() then - - self:E( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) - - self:ClearTargetDistance() - AIGroup:ClearTasks() - - local EngageRoute = {} - - --- Calculate the target route point. - - local CurrentCoord = AIGroup:GetCoordinate() - local ToTargetCoord = self.HomeAirbase:GetCoordinate() - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - local ToAirbaseAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) - - local Distance = CurrentCoord:Get2DDistance( ToTargetCoord ) - - local ToAirbaseCoord = CurrentCoord:Translate( 5000, ToAirbaseAngle ) - if Distance < 5000 then - self:E( "RTB and near the airbase!" ) - self:Home() - return - end - --- Create a route point of type air. - local ToRTBRoutePoint = ToAirbaseCoord:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = ToAirbaseAngle, ToTargetSpeed = ToTargetSpeed } ) - self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToRTBRoutePoint - EngageRoute[#EngageRoute+1] = ToRTBRoutePoint - - AIGroup:OptionROEHoldFire() - AIGroup:OptionROTEvadeFire() - - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - AIGroup:WayPointInitialize( EngageRoute ) - - local Tasks = {} - Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_AIR.RTBRoute", self ) - EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) - - --- NOW ROUTE THE GROUP! - AIGroup:Route( EngageRoute, 0.5 ) - - end - -end - ---- @param #AI_AIR self --- @param Wrapper.Group#GROUP AIGroup -function AI_AIR:onafterHome( AIGroup, From, Event, To ) - self:F( { AIGroup, From, Event, To } ) - - self:E( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) - - if AIGroup and AIGroup:IsAlive() then - end - -end - - - ---- @param #AI_AIR self --- @param Wrapper.Group#GROUP AIGroup -function AI_AIR:onafterHold( AIGroup, From, Event, To, HoldTime ) - self:F( { AIGroup, From, Event, To } ) - - self:E( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" ) - - if AIGroup and AIGroup:IsAlive() then - local OrbitTask = AIGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) - local TimedOrbitTask = AIGroup:TaskControlled( OrbitTask, AIGroup:TaskCondition( nil, nil, nil, nil, HoldTime , nil ) ) - - local RTBTask = AIGroup:TaskFunction( "AI_AIR.RTBHold", self ) - - local OrbitHoldTask = AIGroup:TaskOrbitCircle( 4000, self.PatrolMinSpeed ) - - --AIGroup:SetState( AIGroup, "AI_AIR", self ) - - AIGroup:SetTask( AIGroup:TaskCombo( { TimedOrbitTask, RTBTask, OrbitHoldTask } ), 1 ) - end - -end - ---- @param Wrapper.Group#GROUP AIGroup -function AI_AIR.Resume( AIGroup, Fsm ) - - AIGroup:I( { "AI_AIR.Resume:", AIGroup:GetName() } ) - if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) - end - -end - ---- @param #AI_AIR self --- @param Wrapper.Group#GROUP AIGroup -function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) - self:F( { AIGroup, From, Event, To } ) - - self:E( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. " )" ) - - if AIGroup and AIGroup:IsAlive() then - local Tanker = GROUP:FindByName( self.TankerName ) - if Tanker:IsAlive() and Tanker:IsAirPlane() then - - local RefuelRoute = {} - - --- Calculate the target route point. - - local CurrentCoord = AIGroup:GetCoordinate() - local ToRefuelCoord = Tanker:GetCoordinate() - local ToRefuelSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - - --- Create a route point of type air. - local ToRefuelRoutePoint = ToRefuelCoord:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToRefuelSpeed, - true - ) - - self:F( { ToRefuelSpeed = ToRefuelSpeed } ) - - RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint - RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint - - AIGroup:OptionROEHoldFire() - AIGroup:OptionROTEvadeFire() - - local Tasks = {} - Tasks[#Tasks+1] = AIGroup:TaskRefueling() - Tasks[#Tasks+1] = AIGroup:TaskFunction( self:GetClassName() .. ".Resume", self ) - RefuelRoute[#RefuelRoute].task = AIGroup:TaskCombo( Tasks ) - - AIGroup:Route( RefuelRoute, 0.5 ) - else - self:RTB() - end - end - -end - - - ---- @param #AI_AIR self -function AI_AIR:onafterDead() - self:SetStatusOff() -end - - ---- @param #AI_AIR self --- @param Core.Event#EVENTDATA EventData -function AI_AIR:OnCrash( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:E( self.Controllable:GetUnits() ) - if #self.Controllable:GetUnits() == 1 then - self:__Crash( 1, EventData ) - end - end -end - ---- @param #AI_AIR self --- @param Core.Event#EVENTDATA EventData -function AI_AIR:OnEjection( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__Eject( 1, EventData ) - end -end - ---- @param #AI_AIR self --- @param Core.Event#EVENTDATA EventData -function AI_AIR:OnPilotDead( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__PilotDead( 1, EventData ) - end -end diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 6aadabde0..c02096609 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -1,26 +1,26 @@ --- **AI** -- Build large airborne formations of aircraft. --- +-- -- **Features:** -- -- * Build in-air formations consisting of more than 40 aircraft as one group. -- * Build different formation types. -- * Assign a group leader that will guide the large formation path. --- +-- -- === --- +-- -- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/FOR%20-%20Formation) --- +-- -- === --- +-- -- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0bFIJ9jIdYM22uaWmIN4oz) --- +-- -- === --- +-- -- ### Author: **FlightControl** --- ### Contributions: --- +-- ### Contributions: +-- -- === --- +-- -- @module AI.AI_Formation -- @image AI_Large_Formations.JPG @@ -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. @@ -43,33 +44,33 @@ -- AI_FORMATION makes AI @{GROUP}s fly in formation of various compositions. -- The AI_FORMATION class models formations in a different manner than the internal DCS formation logic!!! -- The purpose of the class is to: --- +-- -- * Make formation building a process that can be managed while in flight, rather than a task. -- * Human players can guide formations, consisting of larget planes. -- * Build large formations (like a large bomber field). -- * Form formations that DCS does not support off the shelve. --- +-- -- A few remarks: --- +-- -- * Depending on the type of plane, the change in direction by the leader may result in the formation getting disentangled while in flight and needs to be rebuild. -- * Formations are vulnerable to collissions, but is depending on the type of plane, the distance between the planes and the speed and angle executed by the leader. -- * Formations may take a while to build up. --- +-- -- As a result, the AI_FORMATION is not perfect, but is very useful to: --- +-- -- * Model large formations when flying straight line. You can build close formations when doing this. -- * Make humans guide a large formation, when the planes are wide from each other. --- +-- -- ## AI_FORMATION construction --- +-- -- Create a new SPAWN object with the @{#AI_FORMATION.New} method: -- -- * @{#AI_FORMATION.New}(): Creates a new AI_FORMATION object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT} or a @{Wrapper.Unit#UNIT}, with an optional briefing text. -- -- ## Formation methods --- +-- -- The following methods can be used to set or change the formation: --- +-- -- * @{#AI_FORMATION.FormationLine}(): Form a line formation (core formation function). -- * @{#AI_FORMATION.FormationTrail}(): Form a trail formation. -- * @{#AI_FORMATION.FormationLeftLine}(): Form a left line formation. @@ -79,9 +80,9 @@ -- * @{#AI_FORMATION.FormationCenterWing}(): Form a center wing formation. -- * @{#AI_FORMATION.FormationCenterVic}(): Form a Vic formation (same as CenterWing. -- * @{#AI_FORMATION.FormationCenterBoxed}(): Form a center boxed formation. --- +-- -- ## Randomization --- +-- -- Use the method @{AI.AI_Formation#AI_FORMATION.SetFlightRandomization}() to simulate the formation flying errors that pilots make while in formation. Is a range set in meters. -- -- @usage @@ -91,8 +92,8 @@ -- local LargeFormation = AI_FORMATION:New( LeaderUnit, FollowGroupSet, "Center Wing Formation", "Briefing" ) -- LargeFormation:FormationCenterWing( 500, 50, 0, 250, 250 ) -- LargeFormation:__Start( 1 ) --- --- @field #AI_FORMATION +-- +-- @field #AI_FORMATION AI_FORMATION = { ClassName = "AI_FORMATION", FollowName = nil, -- The Follow Name @@ -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 @@ -133,14 +135,14 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin self.FollowUnit = FollowUnit -- Wrapper.Unit#UNIT self.FollowGroupSet = FollowGroupSet -- Core.Set#SET_GROUP - + self:SetFlightRandomization( 2 ) - - self:SetStartState( "None" ) + + self:SetStartState( "None" ) self:AddTransition( "*", "Stop", "Stopped" ) - self:AddTransition( "None", "Start", "Following" ) + self:AddTransition( {"None", "Stopped"}, "Start", "Following" ) self:AddTransition( "*", "FormationLine", "*" ) --- FormationLine Handler OnBefore for AI_FORMATION @@ -157,7 +159,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -- @return #boolean - + --- FormationLine Handler OnAfter for AI_FORMATION -- @function [parent=#AI_FORMATION] OnAfterFormationLine -- @param #AI_FORMATION self @@ -171,7 +173,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YSpace The space between groups on the Y-axis in meters for each sequent group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationLine Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] FormationLine -- @param #AI_FORMATION self @@ -181,7 +183,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YSpace The space between groups on the Y-axis in meters for each sequent group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationLine Asynchronous Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] __FormationLine -- @param #AI_FORMATION self @@ -192,7 +194,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YSpace The space between groups on the Y-axis in meters for each sequent group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + self:AddTransition( "*", "FormationTrail", "*" ) --- FormationTrail Handler OnBefore for AI_FORMATION -- @function [parent=#AI_FORMATION] OnBeforeFormationTrail @@ -204,7 +206,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @return #boolean - + --- FormationTrail Handler OnAfter for AI_FORMATION -- @function [parent=#AI_FORMATION] OnAfterFormationTrail -- @param #AI_FORMATION self @@ -214,14 +216,14 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #number XStart The start position on the X-axis in meters for the first group. -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - + --- FormationTrail Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] FormationTrail -- @param #AI_FORMATION self -- @param #number XStart The start position on the X-axis in meters for the first group. -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. - + --- FormationTrail Asynchronous Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] __FormationTrail -- @param #AI_FORMATION self @@ -242,7 +244,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. -- @return #boolean - + --- FormationStack Handler OnAfter for AI_FORMATION -- @function [parent=#AI_FORMATION] OnAfterFormationStack -- @param #AI_FORMATION self @@ -253,7 +255,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - + --- FormationStack Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] FormationStack -- @param #AI_FORMATION self @@ -261,7 +263,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - + --- FormationStack Asynchronous Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] __FormationStack -- @param #AI_FORMATION self @@ -271,7 +273,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. - self:AddTransition( "*", "FormationLeftLine", "*" ) + self:AddTransition( "*", "FormationLeftLine", "*" ) --- FormationLeftLine Handler OnBefore for AI_FORMATION -- @function [parent=#AI_FORMATION] OnBeforeFormationLeftLine -- @param #AI_FORMATION self @@ -284,7 +286,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -- @return #boolean - + --- FormationLeftLine Handler OnAfter for AI_FORMATION -- @function [parent=#AI_FORMATION] OnAfterFormationLeftLine -- @param #AI_FORMATION self @@ -296,7 +298,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationLeftLine Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] FormationLeftLine -- @param #AI_FORMATION self @@ -304,7 +306,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationLeftLine Asynchronous Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] __FormationLeftLine -- @param #AI_FORMATION self @@ -314,7 +316,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - self:AddTransition( "*", "FormationRightLine", "*" ) + self:AddTransition( "*", "FormationRightLine", "*" ) --- FormationRightLine Handler OnBefore for AI_FORMATION -- @function [parent=#AI_FORMATION] OnBeforeFormationRightLine -- @param #AI_FORMATION self @@ -327,7 +329,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -- @return #boolean - + --- FormationRightLine Handler OnAfter for AI_FORMATION -- @function [parent=#AI_FORMATION] OnAfterFormationRightLine -- @param #AI_FORMATION self @@ -339,7 +341,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationRightLine Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] FormationRightLine -- @param #AI_FORMATION self @@ -347,7 +349,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationRightLine Asynchronous Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] __FormationRightLine -- @param #AI_FORMATION self @@ -371,7 +373,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -- @return #boolean - + --- FormationLeftWing Handler OnAfter for AI_FORMATION -- @function [parent=#AI_FORMATION] OnAfterFormationLeftWing -- @param #AI_FORMATION self @@ -384,7 +386,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationLeftWing Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] FormationLeftWing -- @param #AI_FORMATION self @@ -393,7 +395,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationLeftWing Asynchronous Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] __FormationLeftWing -- @param #AI_FORMATION self @@ -403,7 +405,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + self:AddTransition( "*", "FormationRightWing", "*" ) --- FormationRightWing Handler OnBefore for AI_FORMATION -- @function [parent=#AI_FORMATION] OnBeforeFormationRightWing @@ -418,7 +420,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -- @return #boolean - + --- FormationRightWing Handler OnAfter for AI_FORMATION -- @function [parent=#AI_FORMATION] OnAfterFormationRightWing -- @param #AI_FORMATION self @@ -431,7 +433,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationRightWing Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] FormationRightWing -- @param #AI_FORMATION self @@ -440,7 +442,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationRightWing Asynchronous Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] __FormationRightWing -- @param #AI_FORMATION self @@ -450,7 +452,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer YStart The start position on the Y-axis in meters for the first group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + self:AddTransition( "*", "FormationCenterWing", "*" ) --- FormationCenterWing Handler OnBefore for AI_FORMATION -- @function [parent=#AI_FORMATION] OnBeforeFormationCenterWing @@ -466,7 +468,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -- @return #boolean - + --- FormationCenterWing Handler OnAfter for AI_FORMATION -- @function [parent=#AI_FORMATION] OnAfterFormationCenterWing -- @param #AI_FORMATION self @@ -480,7 +482,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationCenterWing Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] FormationCenterWing -- @param #AI_FORMATION self @@ -490,7 +492,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationCenterWing Asynchronous Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] __FormationCenterWing -- @param #AI_FORMATION self @@ -516,7 +518,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -- @return #boolean - + --- FormationVic Handler OnAfter for AI_FORMATION -- @function [parent=#AI_FORMATION] OnAfterFormationVic -- @param #AI_FORMATION self @@ -529,7 +531,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationVic Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] FormationVic -- @param #AI_FORMATION self @@ -539,7 +541,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. - + --- FormationVic Asynchronous Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] __FormationVic -- @param #AI_FORMATION self @@ -566,7 +568,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -- @param #number ZLevels The amount of levels on the Z-axis. -- @return #boolean - + --- FormationBox Handler OnAfter for AI_FORMATION -- @function [parent=#AI_FORMATION] OnAfterFormationBox -- @param #AI_FORMATION self @@ -580,7 +582,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -- @param #number ZLevels The amount of levels on the Z-axis. - + --- FormationBox Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] FormationBox -- @param #AI_FORMATION self @@ -591,7 +593,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -- @param #number ZLevels The amount of levels on the Z-axis. - + --- FormationBox Asynchronous Trigger for AI_FORMATION -- @function [parent=#AI_FORMATION] __FormationBox -- @param #AI_FORMATION self @@ -603,12 +605,12 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -- @param #number ZLevels The amount of levels on the Z-axis. - - + + self:AddTransition( "*", "Follow", "Following" ) self:FormationLeftLine( 500, 0, 250, 250 ) - + self.FollowName = FollowName self.FollowBriefing = FollowBriefing @@ -621,6 +623,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 @@ -648,23 +660,23 @@ function AI_FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, X self:F( { FollowGroupSet, From , Event ,To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace } ) FollowGroupSet:Flush( self ) - + local FollowSet = FollowGroupSet:GetSet() - + local i = 1 --FF i=0 caused first unit to have no XSpace! Probably needs further adjustments. This is just a quick work around. - + for FollowID, FollowGroup in pairs( FollowSet ) do - + local PointVec3 = POINT_VEC3:New() PointVec3:SetX( XStart + i * XSpace ) PointVec3:SetY( YStart + i * YSpace ) PointVec3:SetZ( ZStart + i * ZSpace ) - + local Vec3 = PointVec3:GetVec3() FollowGroup:SetState( self, "FormationVec3", Vec3 ) i = i + 1 end - + return self end @@ -800,25 +812,25 @@ end function AI_FORMATION:onafterFormationCenterWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) --R2.1 local FollowSet = FollowGroupSet:GetSet() - + local i = 0 - + for FollowID, FollowGroup in pairs( FollowSet ) do - + local PointVec3 = POINT_VEC3:New() - + local Side = ( i % 2 == 0 ) and 1 or -1 local Row = i / 2 + 1 - + PointVec3:SetX( XStart + Row * XSpace ) PointVec3:SetY( YStart ) PointVec3:SetZ( Side * ( ZStart + i * ZSpace ) ) - + local Vec3 = PointVec3:GetVec3() FollowGroup:SetState( self, "FormationVec3", Vec3 ) i = i + 1 end - + return self end @@ -838,7 +850,7 @@ end function AI_FORMATION:onafterFormationVic( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) --R2.1 self:onafterFormationCenterWing(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace) - + return self end @@ -858,21 +870,21 @@ end function AI_FORMATION:onafterFormationBox( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) --R2.1 local FollowSet = FollowGroupSet:GetSet() - + local i = 0 - + for FollowID, FollowGroup in pairs( FollowSet ) do - + local PointVec3 = POINT_VEC3:New() - + local ZIndex = i % ZLevels local XIndex = math.floor( i / ZLevels ) local YIndex = math.floor( i / ZLevels ) - + PointVec3:SetX( XStart + XIndex * XSpace ) PointVec3:SetY( YStart + YIndex * YSpace ) PointVec3:SetZ( -ZStart - (ZSpace * ZLevels / 2 ) + ZSpace * ZIndex ) - + local Vec3 = PointVec3:GetVec3() FollowGroup:SetState( self, "FormationVec3", Vec3 ) i = i + 1 @@ -889,7 +901,7 @@ end function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 self.FlightRandomization = FlightRandomization - + return self end @@ -939,16 +951,16 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 CT2 = timer.getTime() CV1 = ClientUnit:GetState( self, "CV1" ) CV2 = ClientUnit:GetPointVec3() - + ClientUnit:SetState( self, "CT1", CT2 ) ClientUnit:SetState( self, "CV1", CV2 ) end - + FollowGroupSet:ForEachGroup( --- @param Wrapper.Group#GROUP FollowGroup -- @param Wrapper.Unit#UNIT ClientUnit function( FollowGroup, Formation, ClientUnit, CT1, CV1, CT2, CV2 ) - + FollowGroup:OptionROTEvadeFire() FollowGroup:OptionROEReturnFire() @@ -956,21 +968,21 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 local FollowFormation = FollowGroup:GetState( self, "FormationVec3" ) if FollowFormation then local FollowDistance = FollowFormation.x - + local GT1 = GroupUnit:GetState( self, "GT1" ) - + if CT1 == nil or CT1 == 0 or GT1 == nil or GT1 == 0 then GroupUnit:SetState( self, "GV1", GroupUnit:GetPointVec3() ) - GroupUnit:SetState( self, "GT1", timer.getTime() ) + GroupUnit:SetState( self, "GT1", timer.getTime() ) else local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 local CT = CT2 - CT1 - + local CS = ( 3600 / CT ) * ( CD / 1000 ) / 3.6 local CDv = { x = CV2.x - CV1.x, y = CV2.y - CV1.y, z = CV2.z - CV1.z } local Ca = math.atan2( CDv.x, CDv.z ) - + local GT1 = GroupUnit:GetState( self, "GT1" ) local GT2 = timer.getTime() local GV1 = GroupUnit:GetState( self, "GV1" ) @@ -980,29 +992,29 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 GV2:AddZ( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) GroupUnit:SetState( self, "GT1", GT2 ) GroupUnit:SetState( self, "GV1", GV2 ) - - + + local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 local GT = GT2 - GT1 - + -- Calculate the distance local GDv = { x = GV2.x - CV1.x, y = GV2.y - CV1.y, z = GV2.z - CV1.z } - local Alpha_T = math.atan2( GDv.x, GDv.z ) - math.atan2( CDv.x, CDv.z ) + local Alpha_T = math.atan2( GDv.x, GDv.z ) - math.atan2( CDv.x, CDv.z ) local Alpha_R = ( Alpha_T < 0 ) and Alpha_T + 2 * math.pi or Alpha_T local Position = math.cos( Alpha_R ) local GD = ( ( GDv.x )^2 + ( GDv.z )^2 ) ^ 0.5 local Distance = GD * Position + - CS * 0.5 - + -- Calculate the group direction vector local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } - + -- Calculate GH2, GH2 with the same height as CV2. local GH2 = { x = GV2.x, y = CV2.y + FollowFormation.y, z = GV2.z } - + -- Calculate the angle of GV to the orthonormal plane local alpha = math.atan2( GV.x, GV.z ) - + local GVx = FollowFormation.z * math.cos( Ca ) + FollowFormation.x * math.sin( Ca ) local GVz = FollowFormation.x * math.cos( Ca ) - FollowFormation.z * math.sin( Ca ) @@ -1013,36 +1025,36 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 y = GH2.y - ( Distance + FollowFormation.x ) / 5, -- + FollowFormation.y, z = CV2.z + CS * 10 * math.cos(Ca), } - + -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } - + -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... local DVu = { x = DV.x / FollowDistance, y = DV.y, z = DV.z / FollowDistance } - + -- Now we can calculate the group destination vector GDV. local GDV = { x = CVI.x, y = CVI.y, z = CVI.z } - + local ADDx = FollowFormation.x * math.cos(alpha) - FollowFormation.z * math.sin(alpha) local ADDz = FollowFormation.z * math.cos(alpha) + FollowFormation.x * math.sin(alpha) - - local GDV_Formation = { - x = GDV.x - GVx, - y = GDV.y, + + local GDV_Formation = { + x = GDV.x - GVx, + y = GDV.y, z = GDV.z - GVz } - + if self.SmokeDirectionVector == true then trigger.action.smoke( GDV, trigger.smokeColor.Green ) trigger.action.smoke( GDV_Formation, trigger.smokeColor.White ) end - - - + + + local Time = 60 - + local Speed = - ( Distance + FollowFormation.x ) / Time local GS = Speed + CS if Speed < 0 then @@ -1056,8 +1068,9 @@ 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/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 3bb8a11b1..b79174989 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1,11 +1,11 @@ --- **Core** - Defines an extensive API to manage 3D points in the DCS World 3D simulation space. -- -- ## Features: --- +-- -- * Provides a COORDINATE class, which allows to manage points in 3D space and perform various operations on it. -- * Provides a POINT\_VEC2 class, which is derived from COORDINATE, and allows to manage points in 3D space, but from a Lat/Lon and Altitude perspective. -- * Provides a POINT\_VEC3 class, which is derived from COORDINATE, and allows to manage points in 3D space, but from a X, Z and Y vector perspective. --- +-- -- === -- -- # Demo Missions @@ -40,8 +40,8 @@ do -- COORDINATE --- @type COORDINATE -- @extends Core.Base#BASE - - + + --- Defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. -- -- # 1) Create a COORDINATE object. @@ -84,17 +84,17 @@ do -- COORDINATE -- -- -- # 3) Create markings on the map. - -- - -- Place markers (text boxes with clarifications for briefings, target locations or any other reference point) + -- + -- Place markers (text boxes with clarifications for briefings, target locations or any other reference point) -- on the map for all players, coalitions or specific groups: - -- + -- -- * @{#COORDINATE.MarkToAll}(): Place a mark to all players. -- * @{#COORDINATE.MarkToCoalition}(): Place a mark to a coalition. -- * @{#COORDINATE.MarkToCoalitionRed}(): Place a mark to the red coalition. -- * @{#COORDINATE.MarkToCoalitionBlue}(): Place a mark to the blue coalition. -- * @{#COORDINATE.MarkToGroup}(): Place a mark to a group (needs to have a client in it or a CA group (CA group is bugged)). -- * @{#COORDINATE.RemoveMark}(): Removes a mark from the map. - -- + -- -- # 4) Coordinate calculation methods. -- -- Various calculation methods exist to use or manipulate 3D space. Find below a short description of each method: @@ -124,37 +124,37 @@ do -- COORDINATE -- -- * @{#COORDINATE.GetRandomVec2InRadius}(): Provides a random 2D vector around the current 3D point, in the given inner to outer band. -- * @{#COORDINATE.GetRandomVec3InRadius}(): Provides a random 3D vector around the current 3D point, in the given inner to outer band. - -- + -- -- ## 4.6) LOS between coordinates. - -- + -- -- Calculate if the coordinate has Line of Sight (LOS) with the other given coordinate. -- Mountains, trees and other objects can be positioned between the two 3D points, preventing visibilty in a straight continuous line. -- The method @{#COORDINATE.IsLOS}() returns if the two coodinates have LOS. - -- + -- -- ## 4.7) Check the coordinate position. - -- + -- -- Various methods are available that allow to check if a coordinate is: - -- + -- -- * @{#COORDINATE.IsInRadius}(): in a give radius. -- * @{#COORDINATE.IsInSphere}(): is in a given sphere. -- * @{#COORDINATE.IsAtCoordinate2D}(): is in a given coordinate within a specific precision. - -- - -- + -- + -- -- -- # 5) Measure the simulation environment at the coordinate. - -- + -- -- ## 5.1) Weather specific. - -- + -- -- Within the DCS simulator, a coordinate has specific environmental properties, like wind, temperature, humidity etc. - -- + -- -- * @{#COORDINATE.GetWind}(): Retrieve the wind at the specific coordinate within the DCS simulator. -- * @{#COORDINATE.GetTemperature}(): Retrieve the temperature at the specific height within the DCS simulator. -- * @{#COORDINATE.GetPressure}(): Retrieve the pressure at the specific height within the DCS simulator. - -- + -- -- ## 5.2) Surface specific. - -- + -- -- Within the DCS simulator, the surface can have various objects placed at the coordinate, and the surface height will vary. - -- + -- -- * @{#COORDINATE.GetLandHeight}(): Retrieve the height of the surface (on the ground) within the DCS simulator. -- * @{#COORDINATE.GetSurfaceType}(): Retrieve the surface type (on the ground) within the DCS simulator. -- @@ -168,13 +168,13 @@ do -- COORDINATE -- Route points can be used in the Route methods of the @{Wrapper.Group#GROUP} class. -- -- ## 7) Manage the roads. - -- + -- -- Important for ground vehicle transportation and movement, the method @{#COORDINATE.GetClosestPointToRoad}() will calculate -- the closest point on the nearest road. - -- + -- -- In order to use the most optimal road system to transport vehicles, the method @{#COORDINATE.GetPathOnRoad}() will calculate -- the most optimal path following the road between two coordinates. - -- + -- -- -- -- @@ -186,7 +186,7 @@ do -- COORDINATE -- -- -- ## 9) Coordinate text generation - -- + -- -- -- * @{#COORDINATE.ToStringBR}(): Generates a Bearing & Range text in the format of DDD for DI where DDD is degrees and DI is distance. -- * @{#COORDINATE.ToStringLL}(): Generates a Latutude & Longutude text. @@ -196,13 +196,13 @@ do -- COORDINATE ClassName = "COORDINATE", } - --- @field COORDINATE.WaypointAltType + --- @field COORDINATE.WaypointAltType COORDINATE.WaypointAltType = { BARO = "BARO", RADIO = "RADIO", } - - --- @field COORDINATE.WaypointAction + + --- @field COORDINATE.WaypointAction COORDINATE.WaypointAction = { TurningPoint = "Turning Point", FlyoverPoint = "Fly Over Point", @@ -212,7 +212,7 @@ do -- COORDINATE Landing = "Landing", } - --- @field COORDINATE.WaypointType + --- @field COORDINATE.WaypointType COORDINATE.WaypointType = { TakeOffParking = "TakeOffParking", TakeOffParkingHot = "TakeOffParkingHot", @@ -228,13 +228,13 @@ do -- COORDINATE -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing to the Right. -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the Right. -- @return #COORDINATE - function COORDINATE:New( x, y, z ) + function COORDINATE:New( x, y, z ) local self = BASE:Inherit( self, BASE:New() ) -- #COORDINATE self.x = x self.y = y self.z = z - + return self end @@ -242,13 +242,13 @@ do -- COORDINATE -- @param #COORDINATE self -- @param #COORDINATE Coordinate. -- @return #COORDINATE - function COORDINATE:NewFromCoordinate( Coordinate ) + function COORDINATE:NewFromCoordinate( Coordinate ) local self = BASE:Inherit( self, BASE:New() ) -- #COORDINATE self.x = Coordinate.x self.y = Coordinate.y self.z = Coordinate.z - + return self end @@ -257,10 +257,10 @@ do -- COORDINATE -- @param DCS#Vec2 Vec2 The Vec2 point. -- @param DCS#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. -- @return #COORDINATE - function COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) + function COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) local LandHeight = land.getHeight( Vec2 ) - + LandHeightAdd = LandHeightAdd or 0 LandHeight = LandHeight + LandHeightAdd @@ -276,7 +276,7 @@ do -- COORDINATE -- @param #COORDINATE self -- @param DCS#Vec3 Vec3 The Vec3 point. -- @return #COORDINATE - function COORDINATE:NewFromVec3( Vec3 ) + function COORDINATE:NewFromVec3( Vec3 ) local self = self:New( Vec3.x, Vec3.y, Vec3.z ) -- #COORDINATE @@ -284,7 +284,7 @@ do -- COORDINATE return self end - + --- Return the coordinates of the COORDINATE in Vec3 format. -- @param #COORDINATE self @@ -308,13 +308,13 @@ do -- COORDINATE -- @param #number altitude (Optional) Altitude in meters. Default is the land height at the coordinate. -- @return #COORDINATE function COORDINATE:NewFromLLDD( latitude, longitude, altitude) - + -- Returns a point from latitude and longitude in the vec3 format. local vec3=coord.LLtoLO(latitude, longitude) - + -- Convert vec3 to coordinate object. local _coord=self:NewFromVec3(vec3) - + -- Adjust height if altitude==nil then _coord.y=altitude @@ -325,23 +325,23 @@ do -- COORDINATE return _coord end - + --- Returns if the 2 coordinates are at the same 2D position. -- @param #COORDINATE self -- @param #COORDINATE Coordinate -- @param #number Precision -- @return #boolean true if at the same position. function COORDINATE:IsAtCoordinate2D( Coordinate, Precision ) - + self:F( { Coordinate = Coordinate:GetVec2() } ) self:F( { self = self:GetVec2() } ) - + local x = Coordinate.x local z = Coordinate.z - - return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z + + return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z end - + --- Scan/find objects (units, statics, scenery) within a certain radius around the coordinate using the world.searchObjects() DCS API function. -- @param #COORDINATE self -- @param #number radius (Optional) Scan radius in meters. Default 100 m. @@ -376,7 +376,7 @@ do -- COORDINATE if scanscenery==nil then scanscenery=false end - + --{Object.Category.UNIT, Object.Category.STATIC, Object.Category.SCENERY} local scanobjects={} if scanunits then @@ -388,7 +388,7 @@ do -- COORDINATE if scanscenery then table.insert(scanobjects, Object.Category.SCENERY) end - + -- Found stuff. local Units = {} local Statics = {} @@ -396,40 +396,40 @@ do -- COORDINATE local gotstatics=false local gotunits=false local gotscenery=false - + local function EvaluateZone(ZoneObject) - + if ZoneObject then - + -- Get category of scanned object. local ObjectCategory = ZoneObject:getCategory() - + -- Check for unit or static objects if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() then - + table.insert(Units, UNIT:Find(ZoneObject)) gotunits=true - + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then - + table.insert(Statics, ZoneObject) gotstatics=true - + elseif ObjectCategory==Object.Category.SCENERY then - + table.insert(Scenery, ZoneObject) gotscenery=true - + end - + end - + return true end - + -- Search the world. world.searchObjects(scanobjects, SphereSearch, EvaluateZone) - + for _,unit in pairs(Units) do self:T(string.format("Scan found unit %s", unit:GetName())) end @@ -439,10 +439,10 @@ do -- COORDINATE for _,scenery in pairs(Scenery) do self:T(string.format("Scan found scenery %s", scenery:getTypeName())) end - + return gotunits, gotstatics, gotscenery, Units, Statics, Scenery end - + --- Calculate the distance from a reference @{#COORDINATE}. -- @param #COORDINATE self -- @param #COORDINATE PointVec2Reference The reference @{#COORDINATE}. @@ -528,7 +528,7 @@ do -- COORDINATE return RandomVec3 end - + --- Return the height of the land at the coordinate. -- @param #COORDINATE self -- @return #number @@ -543,8 +543,8 @@ do -- COORDINATE function COORDINATE:SetHeading( Heading ) self.Heading = Heading end - - + + --- Get the heading of the coordinate, if applicable. -- @param #COORDINATE self -- @return #number or nil @@ -552,7 +552,7 @@ do -- COORDINATE return self.Heading end - + --- Set the velocity of the COORDINATE. -- @param #COORDINATE self -- @param #string Velocity Velocity in meters per second. @@ -560,7 +560,7 @@ do -- COORDINATE self.Velocity = Velocity end - + --- Return the velocity of the COORDINATE. -- @param #COORDINATE self -- @return #number Velocity in meters per second. @@ -569,7 +569,7 @@ do -- COORDINATE return Velocity or 0 end - + --- Return velocity text of the COORDINATE. -- @param #COORDINATE self -- @return #string @@ -632,7 +632,7 @@ do -- COORDINATE local SourceVec3 = self:GetVec3() return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 end - + --- Returns the temperature in Degrees Celsius. -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. @@ -649,22 +649,22 @@ do -- COORDINATE --- Returns a text of the temperature according the measurement system @{Settings}. -- The text will reflect the temperature like this: - -- + -- -- - For Russian and European aircraft using the metric system - Degrees Celcius (°C) -- - For Americain aircraft we link to the imperial system - Degrees Farenheit (°F) - -- - -- A text containing a pressure will look like this: - -- - -- - `Temperature: %n.d °C` + -- + -- A text containing a pressure will look like this: + -- + -- - `Temperature: %n.d °C` -- - `Temperature: %n.d °F` - -- + -- -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. -- @return #string Temperature according the measurement system @{Settings}. function COORDINATE:GetTemperatureText( height, Settings ) - + local DegreesCelcius = self:GetTemperature( height ) - + local Settings = Settings or _SETTINGS if DegreesCelcius then @@ -676,7 +676,7 @@ do -- COORDINATE else return " no temperature" end - + return nil end @@ -692,18 +692,18 @@ do -- COORDINATE -- Return Pressure in hPa. return P/100 end - + --- Returns a text of the pressure according the measurement system @{Settings}. -- The text will contain always the pressure in hPa and: - -- + -- -- - For Russian and European aircraft using the metric system - hPa and mmHg -- - For Americain and European aircraft we link to the imperial system - hPa and inHg - -- - -- A text containing a pressure will look like this: - -- - -- - `QFE: x hPa (y mmHg)` + -- + -- A text containing a pressure will look like this: + -- + -- - `QFE: x hPa (y mmHg)` -- - `QFE: x hPa (y inHg)` - -- + -- -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. E.g. set height=0 for QNH. -- @return #string Pressure in hPa and mmHg or inHg depending on the measurement system @{Settings}. @@ -712,7 +712,7 @@ do -- COORDINATE local Pressure_hPa = self:GetPressure( height ) local Pressure_mmHg = Pressure_hPa * 0.7500615613030 local Pressure_inHg = Pressure_hPa * 0.0295299830714 - + local Settings = Settings or _SETTINGS if Pressure_hPa then @@ -724,14 +724,14 @@ do -- COORDINATE else return " no pressure" end - + return nil end - + --- Returns the heading from this to another coordinate. -- @param #COORDINATE self -- @param #COORDINATE ToCoordinate - -- @return #number Heading in degrees. + -- @return #number Heading in degrees. function COORDINATE:HeadingTo(ToCoordinate) local dz=ToCoordinate.z-self.z local dx=ToCoordinate.x-self.x @@ -741,7 +741,7 @@ do -- COORDINATE end return heading end - + --- Returns the wind direction (from) and strength. -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. @@ -751,12 +751,12 @@ do -- COORDINATE local landheight=self:GetLandHeight()+0.1 -- we at 0.1 meters to be sure to be above ground since wind is zero below ground level. local point={x=self.x, y=math.max(height or self.y, landheight), z=self.z} -- get wind velocity vector - local wind = atmosphere.getWind(point) + local wind = atmosphere.getWind(point) local direction = math.deg(math.atan2(wind.z, wind.x)) if direction < 0 then direction = 360 + direction end - -- Convert to direction to from direction + -- Convert to direction to from direction if direction > 180 then direction = direction-180 else @@ -766,37 +766,37 @@ do -- COORDINATE -- Return wind direction and strength km/h. return direction, strength end - + --- Returns the wind direction (from) and strength. -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. -- @return Direction the wind is blowing from in degrees. function COORDINATE:GetWindWithTurbulenceVec3(height) - - -- AGL height if + + -- AGL height if local landheight=self:GetLandHeight()+0.1 -- we at 0.1 meters to be sure to be above ground since wind is zero below ground level. - - -- Point at which the wind is evaluated. + + -- Point at which the wind is evaluated. local point={x=self.x, y=math.max(height or self.y, landheight), z=self.z} - + -- Get wind velocity vector including turbulences. local vec3 = atmosphere.getWindWithTurbulence(point) - + return vec3 - end + end --- Returns a text documenting the wind direction (from) and strength according the measurement system @{Settings}. -- The text will reflect the wind like this: - -- + -- -- - For Russian and European aircraft using the metric system - Wind direction in degrees (°) and wind speed in meters per second (mps). -- - For Americain aircraft we link to the imperial system - Wind direction in degrees (°) and wind speed in knots per second (kps). - -- - -- A text containing a pressure will look like this: - -- - -- - `Wind: %n ° at n.d mps` + -- + -- A text containing a pressure will look like this: + -- + -- - `Wind: %n ° at n.d mps` -- - `Wind: %n ° at n.d kps` - -- + -- -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. -- @return #string Wind direction and strength according the measurement system @{Settings}. @@ -815,7 +815,7 @@ do -- COORDINATE else return " no wind" end - + return nil end @@ -841,9 +841,9 @@ do -- COORDINATE local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), Precision ) - - local s = string.format( '%03d°', AngleDegrees ) - + + local s = string.format( '%03d°', AngleDegrees ) + return s end @@ -863,7 +863,7 @@ do -- COORDINATE else DistanceText = " for " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " miles" end - + return DistanceText end @@ -929,7 +929,7 @@ do -- COORDINATE local BearingText = self:GetBearingText( AngleRadians, 0, Settings ) local DistanceText = self:GetDistanceText( Distance, Settings ) - + local BRText = BearingText .. DistanceText return BRText @@ -1001,17 +1001,17 @@ do -- COORDINATE -- @return #table The route point. function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked, airbase, DCSTasks, description ) self:F2( { AltType, Type, Action, Speed, SpeedLocked } ) - + -- Defaults AltType=AltType or "RADIO" if SpeedLocked==nil then SpeedLocked=true end Speed=Speed or 500 - + -- Waypoint array. local RoutePoint = {} - + -- Coordinates. RoutePoint.x = self.x RoutePoint.y = self.z @@ -1025,7 +1025,7 @@ do -- COORDINATE RoutePoint.speed = Speed/3.6 RoutePoint.speed_locked = SpeedLocked RoutePoint.ETA=nil - RoutePoint.ETA_locked = false + RoutePoint.ETA_locked = false -- Waypoint description. RoutePoint.name=description -- Airbase parameters for takeoff and landing points. @@ -1036,12 +1036,12 @@ do -- COORDINATE RoutePoint.linkUnit = AirbaseID RoutePoint.helipadId = AirbaseID elseif AirbaseCategory == Airbase.Category.AIRDROME then - RoutePoint.airdromeId = AirbaseID + RoutePoint.airdromeId = AirbaseID else self:T("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!") - end - end - + end + end + -- ["task"] = -- { @@ -1076,7 +1076,7 @@ do -- COORDINATE return self:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true, nil, DCSTasks, description ) end - + --- Build a Waypoint Air "Fly Over Point". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. @@ -1085,8 +1085,8 @@ do -- COORDINATE function COORDINATE:WaypointAirFlyOverPoint( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.FlyoverPoint, Speed ) end - - + + --- Build a Waypoint Air "Take Off Parking Hot". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. @@ -1095,7 +1095,7 @@ do -- COORDINATE function COORDINATE:WaypointAirTakeOffParkingHot( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOffParkingHot, COORDINATE.WaypointAction.FromParkingAreaHot, Speed ) end - + --- Build a Waypoint Air "Take Off Parking". -- @param #COORDINATE self @@ -1105,8 +1105,8 @@ do -- COORDINATE function COORDINATE:WaypointAirTakeOffParking( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, Speed ) end - - + + --- Build a Waypoint Air "Take Off Runway". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. @@ -1115,26 +1115,29 @@ do -- COORDINATE function COORDINATE:WaypointAirTakeOffRunway( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOff, COORDINATE.WaypointAction.FromRunway, Speed ) end - - + + --- 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 - -- + -- -- LandingZone = ZONE:New( "LandingZone" ) -- LandingCoord = LandingZone:GetCoordinate() -- LandingWaypoint = LandingCoord:WaypointAirLanding( 60 ) -- HeliGroup:Route( { LandWaypoint }, 1 ) -- Start landing the helicopter in one second. - -- + -- function COORDINATE:WaypointAirLanding( Speed, airbase, DCSTasks, description ) return self:WaypointAir(nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, nil, airbase, DCSTasks, description) end - - - - + + + + --- Build an ground type route point. -- @param #COORDINATE self -- @param #number Speed (optional) Speed in km/h. The default speed is 20 km/h. @@ -1143,7 +1146,7 @@ do -- COORDINATE function COORDINATE:WaypointGround( Speed, Formation ) self:F2( { Formation, Speed } ) - + local RoutePoint = {} RoutePoint.x = self.x RoutePoint.y = self.z @@ -1183,10 +1186,10 @@ do -- COORDINATE -- @return Wrapper.Airbase#AIRBASE Closest Airbase to the given coordinate. -- @return #number Distance to the closest airbase in meters. function COORDINATE:GetClosestAirbase(Category, Coalition) - + -- Get all airbases of the map. local airbases=AIRBASE.GetAllAirbases(Coalition) - + local closest=nil local distmin=nil -- Loop over all airbases. @@ -1202,14 +1205,14 @@ do -- COORDINATE if dist=2 then for i=1,#Path-1 do @@ -1386,8 +1389,8 @@ do -- COORDINATE else -- There are cases where no path on road can be found. return nil,nil - end - + end + return Path, Way, GotPath end @@ -1531,7 +1534,7 @@ do -- COORDINATE density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmokeAndFire, density) end - + --- Large smoke and fire at the coordinate. -- @param #COORDINATE self -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. @@ -1549,7 +1552,7 @@ do -- COORDINATE density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmokeAndFire, density) end - + --- Small smoke at the coordinate. -- @param #COORDINATE self -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. @@ -1558,7 +1561,7 @@ do -- COORDINATE density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmoke, density) end - + --- Medium smoke at the coordinate. -- @param #COORDINATE self -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. @@ -1576,7 +1579,7 @@ do -- COORDINATE density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmoke, density) end - + --- Huge smoke at the coordinate. -- @param #COORDINATE self -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. @@ -1584,7 +1587,7 @@ do -- COORDINATE self:F2( { density=density } ) density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmoke, density) - end + end --- Flares the point in a color. -- @param #COORDINATE self @@ -1625,9 +1628,9 @@ do -- COORDINATE self:F2( Azimuth ) self:Flare( FLARECOLOR.Red, Azimuth ) end - + do -- Markings - + --- Mark to All -- @param #COORDINATE self -- @param #string MarkText Free format text that shows the marking clarification. @@ -1713,7 +1716,7 @@ do -- COORDINATE trigger.action.markToGroup( MarkID, MarkText, self:GetVec3(), MarkGroup:GetID(), ReadOnly, text ) return MarkID end - + --- Remove a mark -- @param #COORDINATE self -- @param #number MarkID The ID of the mark to be removed. @@ -1726,9 +1729,9 @@ do -- COORDINATE function COORDINATE:RemoveMark( MarkID ) trigger.action.removeMark( MarkID ) end - + end -- Markings - + --- Returns if a Coordinate has Line of Sight (LOS) with the ToCoordinate. -- @param #COORDINATE self @@ -1758,7 +1761,7 @@ do -- COORDINATE local InVec2 = self:GetVec2() local Vec2 = Coordinate:GetVec2() - + local InRadius = UTILS.IsInRadius( InVec2, Vec2, Radius) return InRadius @@ -1775,7 +1778,7 @@ do -- COORDINATE local InVec3 = self:GetVec3() local Vec3 = Coordinate:GetVec3() - + local InSphere = UTILS.IsInSphere( InVec3, Vec3, Radius) return InSphere @@ -1829,7 +1832,7 @@ do -- COORDINATE local Heading = self.Heading local DirectionVec3 = self:GetDirectionVec3( TargetCoordinate ) local Angle = self:GetAngleDegrees( DirectionVec3 ) - + if Heading then local Aspect = Angle - Heading if Aspect > -135 and Aspect <= -45 then @@ -1852,7 +1855,7 @@ do -- COORDINATE -- @param #COORDINATE self -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The LL DMS Text - function COORDINATE:ToStringLLDMS( Settings ) + function COORDINATE:ToStringLLDMS( Settings ) local LL_Accuracy = Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy local lat, lon = coord.LOtoLL( self:GetVec3() ) @@ -1892,11 +1895,11 @@ do -- COORDINATE -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The coordinate Text in the configured coordinate system. function COORDINATE:ToStringFromRP( ReferenceCoord, ReferenceName, Controllable, Settings ) - + self:F2( { ReferenceCoord = ReferenceCoord, ReferenceName = ReferenceName } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS - + local IsAir = Controllable and Controllable:IsAirPlane() or false if IsAir then @@ -1910,7 +1913,7 @@ do -- COORDINATE local Distance = self:Get2DDistance( ReferenceCoord ) return "Target are located " .. self:GetBRText( AngleRadians, Distance, Settings ) .. " from " .. ReferenceName end - + return nil end @@ -1920,8 +1923,8 @@ do -- COORDINATE -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The coordinate Text in the configured coordinate system. - function COORDINATE:ToStringA2G( Controllable, Settings ) - + function COORDINATE:ToStringA2G( Controllable, Settings ) + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -1956,7 +1959,7 @@ do -- COORDINATE -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The coordinate Text in the configured coordinate system. function COORDINATE:ToStringA2A( Controllable, Settings ) -- R2.2 - + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -1964,7 +1967,7 @@ do -- COORDINATE if Settings:IsA2A_BRAA() then if Controllable then local Coordinate = Controllable:GetCoordinate() - return self:ToStringBRA( Coordinate, Settings ) + return self:ToStringBRA( Coordinate, Settings ) else return self:ToStringMGRS( Settings ) end @@ -1996,14 +1999,14 @@ do -- COORDINATE -- @param Tasking.Task#TASK Task The task for which coordinates need to be calculated. -- @return #string The coordinate Text in the configured coordinate system. function COORDINATE:ToString( Controllable, Settings, Task ) - + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS local ModeA2A = false self:E('A2A false') - + if Task then self:E('Task ' .. Task.ClassName ) if Task:IsInstanceOf( TASK_A2A ) then @@ -2028,14 +2031,14 @@ do -- COORDINATE ModeA2A = false end end - + if ModeA2A == true then return self:ToStringA2A( Controllable, Settings ) else return self:ToStringA2G( Controllable, Settings ) end - + return nil end @@ -2048,7 +2051,7 @@ do -- COORDINATE -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The pressure text in the configured measurement system. function COORDINATE:ToStringPressure( Controllable, Settings ) -- R2.3 - + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -2064,7 +2067,7 @@ do -- COORDINATE -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The wind text in the configured measurement system. function COORDINATE:ToStringWind( Controllable, Settings ) - + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -2077,10 +2080,10 @@ do -- COORDINATE -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. -- @param #COORDINATE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS + -- @param Core.Settings#SETTINGS -- @return #string The temperature text in the configured measurement system. function COORDINATE:ToStringTemperature( Controllable, Settings ) - + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -2103,8 +2106,8 @@ do -- POINT_VEC3 -- @field #POINT_VEC3.RoutePointType RoutePointType -- @field #POINT_VEC3.RoutePointAction RoutePointAction -- @extends #COORDINATE - - + + --- Defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. -- -- **Important Note:** Most of the functions in this section were taken from MIST, and reworked to OO concepts. @@ -2190,7 +2193,7 @@ do -- POINT_VEC3 local self = BASE:Inherit( self, COORDINATE:New( x, y, z ) ) -- Core.Point#POINT_VEC3 self:F2( self ) - + return self end @@ -2216,7 +2219,7 @@ do -- POINT_VEC3 local self = BASE:Inherit( self, COORDINATE:NewFromVec3( Vec3 ) ) -- Core.Point#POINT_VEC3 self:F2( self ) - + return self end @@ -2315,7 +2318,7 @@ do -- POINT_VEC2 -- @field DCS#Distance x The x coordinate in meters. -- @field DCS#Distance y the y coordinate in meters. -- @extends Core.Point#COORDINATE - + --- Defines a 2D point in the simulator. The height coordinate (if needed) will be the land height + an optional added height specified. -- -- ## POINT_VEC2 constructor @@ -2343,7 +2346,7 @@ do -- POINT_VEC2 POINT_VEC2 = { ClassName = "POINT_VEC2", } - + --- POINT_VEC2 constructor. @@ -2528,3 +2531,5 @@ do -- POINT_VEC2 end end + + diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 0864fb6c1..199688c19 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -409,9 +409,9 @@ do -- SET_BASE for ObjectID, ObjectData in pairs( self.Set ) do if NearestObject == nil then NearestObject = ObjectData - ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() ) + ClosestDistance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) else - local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() ) + local Distance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) if Distance < ClosestDistance then NearestObject = ObjectData ClosestDistance = Distance diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 466e6a70f..11aaf062d 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1702,7 +1702,7 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT -- When spawned in the air, we need to generate a Takeoff Event. if Takeoff == GROUP.Takeoff.Air then for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do - SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() } , 5 ) + SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() } , 1 ) end end @@ -2005,10 +2005,10 @@ end function SPAWN:InitUnControlled( UnControlled ) self:F2( { self.SpawnTemplatePrefix, UnControlled } ) - self.SpawnUnControlled = UnControlled or true + self.SpawnUnControlled = UnControlled for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = self.SpawnUnControlled + self.SpawnGroups[SpawnGroupID].UnControlled = UnControlled end return self diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index e0971ee06..c1370b2a1 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -618,9 +618,6 @@ function ZONE_RADIUS:GetVec3( Height ) end - - - --- Scan the zone for the presence of units of the given ObjectCategories. -- Note that after a zone has been scanned, the zone can be evaluated by: -- @@ -632,11 +629,11 @@ end -- @{#ZONE_RADIUS. -- @param #ZONE_RADIUS self -- @param ObjectCategories --- @param UnitCategories +-- @param Coalition -- @usage -- self.Zone:Scan() -- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) -function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) +function ZONE_RADIUS:Scan( ObjectCategories ) self.ScanData = {} self.ScanData.Coalitions = {} @@ -663,24 +660,9 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then local CoalitionDCSUnit = ZoneObject:getCoalition() - local Include = false - if not UnitCategories then - Include = true - else - local CategoryDCSUnit = ZoneObject:getDesc().category - for UnitCategoryID, UnitCategory in pairs( UnitCategories ) do - if UnitCategory == CategoryDCSUnit then - Include = true - break - end - end - end - if Include then - local CoalitionDCSUnit = ZoneObject:getCoalition() - self.ScanData.Coalitions[CoalitionDCSUnit] = true - self.ScanData.Units[ZoneObject] = ZoneObject - self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) - end + self.ScanData.Coalitions[CoalitionDCSUnit] = true + self.ScanData.Units[ZoneObject] = ZoneObject + self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) end if ObjectCategory == Object.Category.SCENERY then local SceneryType = ZoneObject:getTypeName() 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/Designate.lua b/Moose Development/Moose/Functional/Designate.lua index b8b1c24bb..d917d1cd3 100644 --- a/Moose Development/Moose/Functional/Designate.lua +++ b/Moose Development/Moose/Functional/Designate.lua @@ -286,9 +286,9 @@ do -- DESIGNATE -- * The status report can be automatically flashed by selecting "Status" -> "Flash Status On". -- * The automatic flashing of the status report can be deactivated by selecting "Status" -> "Flash Status Off". -- * The flashing of the status menu is disabled by default. - -- * The method @{#DESIGNATE.SetFlashStatusMenu}() can be used to enable or disable to flashing of the status menu. + -- * The method @{#DESIGNATE.FlashStatusMenu}() can be used to enable or disable to flashing of the status menu. -- - -- Designate:SetFlashStatusMenu( true ) + -- Designate:FlashStatusMenu( true ) -- -- The example will activate the flashing of the status menu for this Designate object. -- @@ -474,7 +474,7 @@ do -- DESIGNATE self.Designating = {} self:SetDesignateName() - self:SetLaseDuration() -- Default is 120 seconds. + self.LaseDuration = 60 self:SetFlashStatusMenu( false ) self:SetFlashDetectionMessages( true ) @@ -677,14 +677,6 @@ do -- DESIGNATE return self end - --- Set the lase duration for designations. - -- @param #DESIGNATE self - -- @param #number LaseDuration The time in seconds a lase will continue to hold on target. The default is 120 seconds. - -- @return #DESIGNATE - function DESIGNATE:SetLaseDuration( LaseDuration ) - self.LaseDuration = LaseDuration or 120 - return self - end --- Generate an array of possible laser codes. -- Each new lase will select a code from this table. @@ -1008,9 +1000,9 @@ do -- DESIGNATE if string.find( Designating, "L", 1, true ) == nil then MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Search other target", DetectedMenu, self.MenuForget, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) for LaserCode, MenuText in pairs( self.MenuLaserCodes ) do - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, string.format( MenuText, LaserCode ), DetectedMenu, self.MenuLaseCode, self, DesignateIndex, self.LaseDuration, LaserCode ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, string.format( MenuText, LaserCode ), DetectedMenu, self.MenuLaseCode, self, DesignateIndex, 60, LaserCode ):SetTime( MenuTime ):SetTag( self.DesignateName ) end - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Lase with random laser code(s)", DetectedMenu, self.MenuLaseOn, self, DesignateIndex, self.LaseDuration ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Lase with random laser code(s)", DetectedMenu, self.MenuLaseOn, self, DesignateIndex, 60 ):SetTime( MenuTime ):SetTag( self.DesignateName ) else MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Stop lasing", DetectedMenu, self.MenuLaseOff, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) end @@ -1168,10 +1160,10 @@ do -- DESIGNATE if string.find( self.Designating[Index], "L", 1, true ) == nil then self.Designating[Index] = self.Designating[Index] .. "L" - self.LaseStart = timer.getTime() - self.LaseDuration = Duration - self:Lasing( Index, Duration, LaserCode ) end + self.LaseStart = timer.getTime() + self.LaseDuration = Duration + self:Lasing( Index, Duration, LaserCode ) end @@ -1330,7 +1322,7 @@ do -- DESIGNATE local MarkedLaserCodesText = ReportLaserCodes:Text(', ') self.CC:GetPositionable():MessageToSetGroup( "Marking " .. MarkingCount .. " x " .. MarkedTypesText .. ", code " .. MarkedLaserCodesText .. ".", 5, self.AttackSet, self.DesignateName ) - self:__Lasing( -self.LaseDuration, Index, Duration, LaserCodeRequested ) + self:__Lasing( -30, Index, Duration, LaserCodeRequested ) self:SetDesignateMenu() 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/Range.lua b/Moose Development/Moose/Functional/Range.lua index 5eb834743..394f134f2 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -1,39 +1,39 @@ --- **Functional** - Range Practice. --- +-- -- === --- +-- -- The RANGE class enables easy set up of bombing and strafing ranges within DCS World. --- +-- -- Implementation is based on the [Simple Range Script](https://forums.eagle.ru/showthread.php?t=157991) by [Ciribob](https://forums.eagle.ru/member.php?u=112175), which itself was motivated -- by a script by SNAFU [see here](https://forums.eagle.ru/showthread.php?t=109174). --- +-- -- [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is highly recommended for this class. --- +-- -- ## Features: -- -- * Impact points of bombs, rockets and missils are recorded and distance to closest range target is measured and reported to the player. --- * Number of hits on strafing passes are counted and reported. Also the percentage of hits w.r.t fired shots is evaluated. --- * Results of all bombing and strafing runs are stored and top 10 results can be displayed. +-- * Number of hits on strafing passes are counted and reported. Also the percentage of hits w.r.t fired shots is evaluated. +-- * Results of all bombing and strafing runs are stored and top 10 results can be displayed. -- * Range targets can be marked by smoke. -- * Range can be illuminated by illumination bombs for night practices. -- * Bomb, rocket and missile impact points can be marked by smoke. -- * Direct hits on targets can trigger flares. -- * Smoke and flare colors can be adjusted for each player via radio menu. -- * Range information and weather report at the range can be reported via radio menu. --- +-- -- More information and examples can be found below. --- +-- -- === --- --- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) -- ### [MOOSE - On the Range - Demonstration Video](https://www.youtube.com/watch?v=kIXcxNB9_3M) --- +-- -- === --- +-- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** --- +-- -- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536), [Ciribob](https://forums.eagle.ru/member.php?u=112175) --- +-- -- === -- @module Functional.Range -- @image Range.JPG @@ -46,7 +46,7 @@ -- @field #string rangename Name of the range. -- @field Core.Point#COORDINATE location Coordinate of the range location. -- @field #number rangeradius Radius of range defining its total size for e.g. smoking bomb impact points and sending radio messages. Default 5 km. --- @field Core.Zone#ZONE rangezone MOOSE zone object of the range. For example, no bomb impacts are smoked if bombs fall outside of the range zone. +-- @field Core.Zone#ZONE rangezone MOOSE zone object of the range. For example, no bomb impacts are smoked if bombs fall outside of the range zone. -- @field #table strafeTargets Table of strafing targets. -- @field #table bombingTargets Table of targets to bomb. -- @field #number nbombtargets Number of bombing targets. @@ -62,11 +62,11 @@ -- @field #number Tmsg Time [sec] messages to players are displayed. Default 30 sec. -- @field #string examinergroupname Name of the examiner group which should get all messages. -- @field #boolean examinerexclusive If true, only the examiner gets messages. If false, clients and examiner get messages. --- @field #number strafemaxalt Maximum altitude above ground for registering for a strafe run. Default is 914 m = 3000 ft. +-- @field #number strafemaxalt Maximum altitude above ground for registering for a strafe run. Default is 914 m = 3000 ft. -- @field #number ndisplayresult Number of (player) results that a displayed. Default is 10. -- @field Utilities.Utils#SMOKECOLOR BombSmokeColor Color id used for smoking bomb targets. -- @field Utilities.Utils#SMOKECOLOR StrafeSmokeColor Color id used to smoke strafe targets. --- @field Utilities.Utils#SMOKECOLOR StrafePitSmokeColor Color id used to smoke strafe pit approach boxes. +-- @field Utilities.Utils#SMOKECOLOR StrafePitSmokeColor Color id used to smoke strafe pit approach boxes. -- @field #number illuminationminalt Minimum altitude AGL in meters at which illumination bombs are fired. Default is 500 m. -- @field #number illuminationmaxalt Maximum altitude AGL in meters at which illumination bombs are fired. Default is 1000 m. -- @field #number scorebombdistance Distance from closest target up to which bomb hits are counted. Default 1000 m. @@ -79,26 +79,26 @@ --- Enables a mission designer to easily set up practice ranges in DCS. A new RANGE object can be created with the @{#RANGE.New}(rangename) contructor. -- The parameter "rangename" defindes the name of the range. It has to be unique since this is also the name displayed in the radio menu. --- +-- -- Generally, a range consists of strafe pits and bombing targets. For strafe pits the number of hits for each pass is counted and tabulated. -- For bombing targets, the distance from the impact point of the bomb, rocket or missile to the closest range target is measured and tabulated. -- Each player can display his best results via a function in the radio menu or see the best best results from all players. --- +-- -- When all targets have been defined in the script, the range is started by the @{#RANGE.Start}() command. --- +-- -- **IMPORTANT** --- +-- -- Due to a DCS bug, it is not possible to directly monitor when a player enters a plane. So in a mission with client slots, it is vital that -- a player first enters as spector and **after that** jumps into the slot of his aircraft! -- If that is not done, the script is not started correctly. This can be checked by looking at the radio menues. If the mission was entered correctly, --- there should be an "On the Range" menu items in the "F10. Other..." menu. --- +-- there should be an "On the Range" menu items in the "F10. Other..." menu. +-- -- ## Strafe Pits -- Each strafe pit can consist of multiple targets. Often one findes two or three strafe targets next to each other. --- +-- -- A strafe pit can be added to the range by the @{#RANGE.AddStrafePit}(*targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*) function. --- --- * The first parameter *targetnames* defines the target or targets. This has to be given as a lua table which contains the names of @{Wrapper.Unit} or @{Static} objects defined in the mission editor. +-- +-- * The first parameter *targetnames* defines the target or targets. This has to be given as a lua table which contains the names of @{Wrapper.Unit} or @{Static} objects defined in the mission editor. -- * In order to perform a valid pass on the strafe pit, the pilot has to begin his run from the correct direction. Therefore, an "approach box" is defined in front -- of the strafe targets. The parameters *boxlength* and *boxwidth* define the size of the box while the parameter *heading* defines its direction. -- If the parameter *heading* is passed as **nil**, the heading is automatically taken from the heading of the first target unit as defined in the ME. @@ -106,107 +106,107 @@ -- wrong/opposite direction. -- * The parameter *goodpass* defines the number of hits a pilot has to achive during a run to be judged as a "good" pass. -- * The last parameter *foulline* sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted! --- +-- -- Another function to add a strafe pit is @{#RANGE.AddStrafePitGroup}(*group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*). Here, -- the first parameter *group* is a MOOSE @{Wrapper.Group} object and **all** units in this group define **one** strafe pit. --- +-- -- Finally, a valid approach has to be performed below a certain maximum altitude. The default is 914 meters (3000 ft) AGL. This is a parameter valid for all -- strafing pits of the range and can be adjusted by the @{#RANGE.SetMaxStrafeAlt}(maxalt) function. --- +-- -- ## Bombing targets -- One ore multiple bombing targets can be added to the range by the @{#RANGE.AddBombingTargets}(targetnames, goodhitrange, randommove) function. --- +-- -- * The first parameter *targetnames* has to be a lua table, which contains the names of @{Wrapper.Unit} and/or @{Static} objects defined in the mission editor. -- Note that the @{Range} logic **automatically** determines, if a name belongs to a @{Wrapper.Unit} or @{Static} object now. -- * The (optional) parameter *goodhitrange* specifies the radius around the target. If a bomb or rocket falls at a distance smaller than this number, the hit is considered to be "good". -- * If final (optional) parameter "*randommove*" can be enabled to create moving targets. If this parameter is set to true, the units of this bombing target will randomly move within the range zone. --- Note that there might be quirks since DCS units can get stuck in buildings etc. So it might be safer to manually define a route for the units in the mission editor if moving targets are desired. --- +-- Note that there might be quirks since DCS units can get stuck in buildings etc. So it might be safer to manually define a route for the units in the mission editor if moving targets are desired. +-- -- Another possibility to add bombing targets is the @{#RANGE.AddBombingTargetGroup}(*group, goodhitrange, randommove*) function. Here the parameter *group* is a MOOSE @{Wrapper.Group} object -- and **all** units in this group are defined as bombing targets. --- +-- -- ## Fine Tuning -- Many range parameters have good default values. However, the mission designer can change these settings easily with the supplied user functions: --- +-- -- * @{#RANGE.SetMaxStrafeAlt}() sets the max altitude for valid strafing runs. -- * @{#RANGE.SetMessageTimeDuration}() sets the duration how long (most) messages are displayed. -- * @{#RANGE.SetDisplayedMaxPlayerResults}() sets the number of results displayed. --- * @{#RANGE.SetRangeRadius}() defines the total range area. --- * @{#RANGE.SetBombTargetSmokeColor}() sets the color used to smoke bombing targets. +-- * @{#RANGE.SetRangeRadius}() defines the total range area. +-- * @{#RANGE.SetBombTargetSmokeColor}() sets the color used to smoke bombing targets. -- * @{#RANGE.SetStrafeTargetSmokeColor}() sets the color used to smoke strafe targets. -- * @{#RANGE.SetStrafePitSmokeColor}() sets the color used to smoke strafe pit approach boxes. -- * @{#RANGE.SetSmokeTimeDelay}() sets the time delay between smoking bomb/rocket impact points after impact. -- * @{#RANGE.TrackBombsON}() or @{#RANGE.TrackBombsOFF}() can be used to enable/disable tracking and evaluating of all bomb types a player fires. -- * @{#RANGE.TrackRocketsON}() or @{#RANGE.TrackRocketsOFF}() can be used to enable/disable tracking and evaluating of all rocket types a player fires. -- * @{#RANGE.TrackMissilesON}() or @{#RANGE.TrackMissilesOFF}() can be used to enable/disable tracking and evaluating of all missile types a player fires. --- +-- -- ## Radio Menu -- Each range gets a radio menu with various submenus where each player can adjust his individual settings or request information about the range or his scores. --- +-- -- The main range menu can be found at "F10. Other..." --> "Fxx. On the Range..." --> "F1. Your Range Name...". -- -- The range menu contains the following submenues: --- --- * "F1. Mark Targets": Various ways to mark targets. +-- +-- * "F1. Mark Targets": Various ways to mark targets. -- * "F2. My Settings": Player specific settings. -- * "F3. Stats" Player: statistics and scores. -- * "Range Information": Information about the range, such as bearing and range. Also range and player specific settings are displayed. --- * "Weather Report": Temperatur, wind and QFE pressure information is provided. --- +-- * "Weather Report": Temperatur, wind and QFE pressure information is provided. +-- -- ## Examples --- +-- -- ### Goldwater Range -- This example shows hot to set up the [Barry M. Goldwater range](https://en.wikipedia.org/wiki/Barry_M._Goldwater_Air_Force_Range). -- It consists of two strafe pits each has two targets plus three bombing targets. --- +-- -- -- Strafe pits. Each pit can consist of multiple targets. Here we have two pits and each of the pits has two targets. -- -- These are names of the corresponding units defined in the ME. -- local strafepit_left={"GWR Strafe Pit Left 1", "GWR Strafe Pit Left 2"} -- local strafepit_right={"GWR Strafe Pit Right 1", "GWR Strafe Pit Right 2"} --- +-- -- -- Table of bombing target names. Again these are the names of the corresponding units as defined in the ME. -- local bombtargets={"GWR Bomb Target Circle Left", "GWR Bomb Target Circle Right", "GWR Bomb Target Hard"} --- +-- -- -- Create a range object. -- GoldwaterRange=RANGE:New("Goldwater Range") --- +-- -- -- Distance between strafe target and foul line. You have to specify the names of the unit or static objects. -- -- Note that this could also be done manually by simply measuring the distance between the target and the foul line in the ME. -- GoldwaterRange:GetFoullineDistance("GWR Strafe Pit Left 1", "GWR Foul Line Left") --- +-- -- -- Add strafe pits. Each pit (left and right) consists of two targets. -- GoldwaterRange:AddStrafePit(strafepit_left, 3000, 300, nil, true, 20, fouldist) -- GoldwaterRange:AddStrafePit(strafepit_right, nil, nil, nil, true, nil, fouldist) --- +-- -- -- Add bombing targets. A good hit is if the bomb falls less then 50 m from the target. -- GoldwaterRange:AddBombingTargets(bombtargets, 50) --- +-- -- -- Start range. -- GoldwaterRange:Start() --- --- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is (implicitly) used in this example. --- +-- +-- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is (implicitly) used in this example. +-- -- ## Debugging --- +-- -- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in -- C:\Users\\Saved Games\DCS\Logs\dcs.log -- All output concerning the RANGE class should have the string "RANGE" in the corresponding line. --- +-- -- The verbosity of the output can be increased by adding the following lines to your script: --- +-- -- BASE:TraceOnOff(true) -- BASE:TraceLevel(1) -- BASE:TraceClass("RANGE") --- +-- -- To get even more output you can increase the trace level to 2 or even 3, c.f. @{BASE} for more details. --- +-- -- The function @{#RANGE.DebugON}() can be used to send messages on screen. It also smokes all defined strafe and bombing targets, the strafe pit approach boxes and the range zone. --- +-- -- Note that it can happen that the RANGE radio menu is not shown. Check that the range object is defined as a **global** variable rather than a local one. --- The could avoid the lua garbage collection to accidentally/falsely deallocate the RANGE objects. --- --- --- +-- The could avoid the lua garbage collection to accidentally/falsely deallocate the RANGE objects. +-- +-- +-- -- @field #RANGE RANGE={ ClassName = "RANGE", @@ -301,16 +301,16 @@ function RANGE:New(rangename) -- Inherit BASE. local self=BASE:Inherit(self, BASE:New()) -- #RANGE - + -- Get range name. --TODO: make sure that the range name is not given twice. This would lead to problems in the F10 radio menu. self.rangename=rangename or "Practice Range" - + -- Debug info. local text=string.format("RANGE script version %s - creating new RANGE object of name: %s.", RANGE.version, self.rangename) self:E(RANGE.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) - + -- Return object. return self end @@ -322,24 +322,24 @@ function RANGE:Start() -- Location/coordinate of range. local _location=nil - + -- Count bomb targets. local _count=0 for _,_target in pairs(self.bombingTargets) do _count=_count+1 - + -- Get range location. if _location==nil then _location=_target.target:GetCoordinate() --Core.Point#COORDINATE end end self.nbombtargets=_count - + -- Count strafing targets. _count=0 for _,_target in pairs(self.strafeTargets) do _count=_count+1 - + for _,_unit in pairs(_target.targets) do if _location==nil then _location=_unit:GetCoordinate() @@ -347,28 +347,28 @@ function RANGE:Start() end end self.nstrafetargets=_count - + -- Location of the range. We simply take the first unit/target we find if it was not explicitly specified by the user. if self.location==nil then self.location=_location end - + if self.location==nil then local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) self:E(RANGE.id..text) return end - + -- Define a MOOSE zone of the range. if self.rangezone==nil then self.rangezone=ZONE_RADIUS:New(self.rangename, {x=self.location.x, y=self.location.z}, self.rangeradius) end - + -- Starting range. local text=string.format("Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) self:E(RANGE.id..text) MESSAGE:New(text,10):ToAllIf(self.Debug) - + -- Event handling. if self.eventmoose then -- Events are handled my MOOSE. @@ -381,20 +381,20 @@ function RANGE:Start() self:T(RANGE.id.."Events are handled directly by DCS.") world.addEventHandler(self) end - + -- Make bomb target move randomly within the range zone. for _,_target in pairs(self.bombingTargets) do -- Check if it is a static object. local _static=self:_CheckStatic(_target.target:GetName()) - + if _target.move and _static==false and _target.speed>1 then local unit=_target.target --Wrapper.Unit#UNIT _target.target:PatrolZones({self.rangezone}, _target.speed*0.75, "Off road") end - + end - + -- Debug mode: smoke all targets and range zone. if self.Debug then self:_MarkTargetsOnMap() @@ -403,7 +403,7 @@ function RANGE:Start() self:_SmokeStrafeTargetBoxes() self.rangezone:SmokeZone(SMOKECOLOR.White) end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -461,7 +461,7 @@ function RANGE:SetBombtrackThreshold(distance) end --- Set range location. If this is not done, one (random) unit position of the range is used to determine the location of the range. --- The range location determines the position at which the weather data is evaluated. +-- The range location determines the position at which the weather data is evaluated. -- @param #RANGE self -- @param Core.Point#COORDINATE coordinate Coordinate of the range. function RANGE:SetRangeLocation(coordinate) @@ -567,44 +567,44 @@ end function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) self:F({targetnames=targetnames, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) - -- Create table if necessary. + -- Create table if necessary. if type(targetnames) ~= "table" then targetnames={targetnames} end - + -- Make targets local _targets={} local center=nil --Wrapper.Unit#UNIT local ntargets=0 - + for _i,_name in ipairs(targetnames) do - + -- Check if we have a static or unit object. local _isstatic=self:_CheckStatic(_name) - local unit=nil + local unit=nil if _isstatic==true then - + -- Add static object. self:T(RANGE.id..string.format("Adding STATIC object %s as strafe target #%d.", _name, _i)) unit=STATIC:FindByName(_name, false) - + elseif _isstatic==false then - + -- Add unit object. self:T(RANGE.id..string.format("Adding UNIT object %s as strafe target #%d.", _name, _i)) unit=UNIT:FindByName(_name) - + else - + -- Neither unit nor static object with this name could be found. local text=string.format("ERROR! Could not find ANY strafe target object with name %s.", _name) self:E(RANGE.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) - + end - - -- Add object to targets. + + -- Add object to targets. if unit then table.insert(_targets, unit) -- Define center as the first unit we find @@ -613,24 +613,24 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe end ntargets=ntargets+1 end - + end - + -- Check if at least one target could be found. if ntargets==0 then local text=string.format("ERROR! No strafe target could be found when calling RANGE:AddStrafePit() for range %s", self.rangename) self:E(RANGE.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) - return + return end -- Approach box dimensions. local l=boxlength or RANGE.Defaults.boxlength local w=(boxwidth or RANGE.Defaults.boxwidth)/2 - + -- Heading: either manually entered or automatically taken from unit heading. local heading=heading or center:GetHeading() - + -- Invert the heading since some units point in the "wrong" direction. In particular the strafe pit from 476th range objects. if inverseheading ~= nil then if inverseheading then @@ -643,42 +643,42 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe if heading>360 then heading=heading-360 end - + -- Number of hits called a "good" pass. goodpass=goodpass or RANGE.Defaults.goodpass - + -- Foule line distance. foulline=foulline or RANGE.Defaults.foulline - + -- Coordinate of the range. local Ccenter=center:GetCoordinate() - + -- Name of the target defined as its unit name. local _name=center:GetName() - -- Points defining the approach area. + -- Points defining the approach area. local p={} p[#p+1]=Ccenter:Translate( w, heading+90) p[#p+1]= p[#p]:Translate( l, heading) p[#p+1]= p[#p]:Translate(2*w, heading-90) p[#p+1]= p[#p]:Translate( -l, heading) - + local pv2={} for i,p in ipairs(p) do pv2[i]={x=p.x, y=p.z} end - + -- Create polygon zone. local _polygon=ZONE_POLYGON_BASE:New(_name, pv2) - + -- Create tires --_polygon:BoundZone() - + -- Add zone to table. table.insert(self.strafeTargets, {name=_name, polygon=_polygon, coordinate= Ccenter, goodPass=goodpass, targets=_targets, foulline=foulline, smokepoints=p, heading=heading}) - + -- Debug info - local text=string.format("Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline) + local text=string.format("Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline) self:T(RANGE.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) end @@ -700,25 +700,25 @@ function RANGE:AddStrafePitGroup(group, boxlength, boxwidth, heading, inversehea self:F({group=group, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) if group and group:IsAlive() then - + -- Get units of group. local _units=group:GetUnits() - + -- Make table of unit names. local _names={} for _,_unit in ipairs(_units) do - + local _unit=_unit --Wrapper.Unit#UNIT - + if _unit and _unit:IsAlive() then local _name=_unit:GetName() table.insert(_names,_name) end - + end - + -- Add strafe pit. - self:AddStrafePit(_names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) + self:AddStrafePit(_names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) end end @@ -735,15 +735,15 @@ function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) if type(targetnames) ~= "table" then targetnames={targetnames} end - + -- Default range is 25 m. goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange - + for _,name in pairs(targetnames) do - + -- Check if we have a static or unit object. local _isstatic=self:_CheckStatic(name) - + if _isstatic==true then local _static=STATIC:FindByName(name) self:T2(RANGE.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange, false)) @@ -755,7 +755,7 @@ function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) else self:E(RANGE.id..string.format("ERROR! Could not find bombing target %s.", name)) end - + end end @@ -766,21 +766,21 @@ end -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) self:F({unit=unit, goodhitrange=goodhitrange, randommove=randommove}) - - -- Get name of positionable. + + -- Get name of positionable. local name=unit:GetName() - + -- Check if we have a static or unit object. local _isstatic=self:_CheckStatic(name) - + -- Default range is 25 m. goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange -- Set randommove to false if it was not specified. if randommove==nil or _isstatic==true then randommove=false - end - + end + -- Debug or error output. if _isstatic==true then self:T(RANGE.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) @@ -789,13 +789,13 @@ function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) else self:E(RANGE.id..string.format("ERROR! No bombing target with name %s could be found. Carefully check all UNIT and STATIC names defined in the mission editor!", name)) end - + -- Get max speed of unit in km/h. local speed=0 if _isstatic==false then speed=self:_GetSpeed(unit) end - + -- Insert target to table. table.insert(self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed}) end @@ -807,18 +807,18 @@ end -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. function RANGE:AddBombingTargetGroup(group, goodhitrange, randommove) self:F({group=group, goodhitrange=goodhitrange, randommove=randommove}) - + if group then - + local _units=group:GetUnits() - + for _,_unit in pairs(_units) do if _unit and _unit:IsAlive() then self:AddBombingTargetUnit(_unit, goodhitrange, randommove) end end end - + end --- Measures the foule line distance between two unit or static objects. @@ -829,10 +829,10 @@ end function RANGE:GetFoullineDistance(namepit, namefoulline) self:F({namepit=namepit, namefoulline=namefoulline}) - -- Check if we have units or statics. + -- Check if we have units or statics. local _staticpit=self:_CheckStatic(namepit) local _staticfoul=self:_CheckStatic(namefoulline) - + -- Get the unit or static pit object. local pit=nil if _staticpit==true then @@ -842,7 +842,7 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) else self:E(RANGE.id..string.format("ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit)) end - + -- Get the unit or static foul line object. local foul=nil if _staticfoul==true then @@ -852,7 +852,7 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) else self:E(RANGE.id..string.format("ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namefoulline)) end - + -- Get the distance between the two objects. local fouldist=0 if pit~=nil and foul~=nil then @@ -890,26 +890,26 @@ function RANGE:onEvent(Event) local EventData={} local _playerunit=nil local _playername=nil - + if Event.initiator then EventData.IniUnitName = Event.initiator:getName() EventData.IniDCSGroup = Event.initiator:getGroup() EventData.IniGroupName = Event.initiator:getGroup():getName() - -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. - _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName) + -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. + _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName) end - if Event.target then + if Event.target then EventData.TgtUnitName = Event.target:getName() EventData.TgtUnit = UNIT:FindByName(EventData.TgtUnitName) end - + if Event.weapon then EventData.Weapon = Event.weapon EventData.weapon = Event.weapon EventData.WeaponTypeName = Event.weapon:getTypeName() - end - + end + -- Event info. self:T3(RANGE.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) self:T3(RANGE.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) @@ -917,22 +917,22 @@ function RANGE:onEvent(Event) self:T3(RANGE.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) self:T3(RANGE.id..string.format("EVENT: Tgt unit = %s" , tostring(EventData.TgtUnitName))) self:T3(RANGE.id..string.format("EVENT: Wpn type = %s" , tostring(EventData.WeaponTypeName))) - + -- Call event Birth function. if Event.id==world.event.S_EVENT_BIRTH and _playername then self:OnEventBirth(EventData) end - + -- Call event Shot function. if Event.id==world.event.S_EVENT_SHOT and _playername and Event.weapon then self:OnEventShot(EventData) end - + -- Call event Hit function. if Event.id==world.event.S_EVENT_HIT and _playername and DCStgtunit then self:OnEventHit(EventData) end - + end @@ -941,34 +941,34 @@ end -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventBirth(EventData) self:F({eventbirth = EventData}) - - local _unitName=EventData.IniUnitName + + local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - + self:T3(RANGE.id.."BIRTH: unit = "..tostring(EventData.IniUnitName)) self:T3(RANGE.id.."BIRTH: group = "..tostring(EventData.IniGroupName)) - self:T3(RANGE.id.."BIRTH: player = "..tostring(_playername)) - + self:T3(RANGE.id.."BIRTH: player = "..tostring(_playername)) + if _unit and _playername then - + local _uid=_unit:GetID() local _group=_unit:GetGroup() local _gid=_group:GetID() local _callsign=_unit:GetCallsign() - + -- Debug output. local text=string.format("Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)", _playername, _callsign, _unitName, _uid, _group:GetName(), _gid) self:T(RANGE.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - + self:_GetAmmo(_unitName) - + -- Reset current strafe status. self.strafeStatus[_uid] = nil - + -- Add Menu commands. self:_AddF10Commands(_unitName) - + -- By default, some bomb impact points and do not flare each hit on target. self.PlayerSettings[_playername]={} self.PlayerSettings[_playername].smokebombimpact=true @@ -976,14 +976,14 @@ function RANGE:OnEventBirth(EventData) self.PlayerSettings[_playername].smokecolor=SMOKECOLOR.Blue self.PlayerSettings[_playername].flarecolor=FLARECOLOR.Red self.PlayerSettings[_playername].delaysmoke=true - + -- Start check in zone timer. if self.planes[_uid] ~= true then SCHEDULER:New(nil, self._CheckInZone, {self, EventData.IniUnitName}, 1, 1) self.planes[_uid] = true end - - end + + end end --- Range event handler for event hit. @@ -991,7 +991,7 @@ end -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventHit(EventData) self:F({eventhit = EventData}) - + -- Debug info. self:T3(RANGE.id.."HIT: Ini unit = "..tostring(EventData.IniUnitName)) self:T3(RANGE.id.."HIT: Ini group = "..tostring(EventData.IniGroupName)) @@ -1003,43 +1003,43 @@ function RANGE:OnEventHit(EventData) if _unit==nil or _playername==nil then return end - + -- Unit ID local _unitID = _unit:GetID() -- Target local target = EventData.TgtUnit local targetname = EventData.TgtUnitName - + -- Current strafe target of player. local _currentTarget = self.strafeStatus[_unitID] -- Player has rolled in on a strafing target. if _currentTarget and target:IsAlive() then - + local playerPos = _unit:GetCoordinate() local targetPos = target:GetCoordinate() -- Loop over valid targets for this run. for _,_target in pairs(_currentTarget.zone.targets) do - + -- Check the the target is the same that was actually hit. if _target and _target:IsAlive() and _target:GetName() == targetname then - + -- Get distance between player and target. local dist=playerPos:Get2DDistance(targetPos) - - if dist > _currentTarget.zone.foulline then + + if dist > _currentTarget.zone.foulline then -- Increase hit counter of this run. _currentTarget.hits = _currentTarget.hits + 1 - + -- Flare target. if _unit and _playername and self.PlayerSettings[_playername].flaredirecthits then targetPos:Flare(self.PlayerSettings[_playername].flarecolor) end else -- Too close to the target. - if _currentTarget.pastfoulline==false and _unit and _playername then + if _currentTarget.pastfoulline==false and _unit and _playername then local _d=_currentTarget.zone.foulline local text=string.format("%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname(_unitName), _d, targetname) self:_DisplayMessageToGroup(_unit, text, 10) @@ -1047,77 +1047,77 @@ function RANGE:OnEventHit(EventData) _currentTarget.pastfoulline=true end end - + end end end - + -- Bombing Targets for _,_bombtarget in pairs(self.bombingTargets) do - + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - + -- Check if one of the bomb targets was hit. if _target and _target:IsAlive() and _bombtarget.name == targetname then - + if _unit and _playername then - + -- Position of target. local targetPos = _target:GetCoordinate() - + -- Message to player. --local text=string.format("%s, direct hit on target %s.", self:_myname(_unitName), targetname) --self:DisplayMessageToGroup(_unit, text, 10, true) - + -- Flare target. if self.PlayerSettings[_playername].flaredirecthits then targetPos:Flare(self.PlayerSettings[_playername].flarecolor) end - + end end end end ---- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). +--- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventShot(EventData) self:F({eventshot = EventData}) - + -- Weapon data. local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName local _weaponStrArray = self:_split(_weapon,"%.") local _weaponName = _weaponStrArray[#_weaponStrArray] - + -- Debug info. self:T(RANGE.id.."EVENT SHOT: Range "..self.rangename) self:T(RANGE.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) self:T(RANGE.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) self:T(RANGE.id.."EVENT SHOT: Weapon type = ".._weapon) self:T(RANGE.id.."EVENT SHOT: Weapon name = ".._weaponName) - + -- Special cases: local _viggen=string.match(_weapon, "ROBOT") or string.match(_weapon, "RB75") or string.match(_weapon, "BK90") or string.match(_weapon, "RB15") or string.match(_weapon, "RB04") - + -- Tracking conditions for bombs, rockets and missiles. - local _bombs=string.match(_weapon, "weapons.bombs") - local _rockets=string.match(_weapon, "weapons.nurs") + local _bombs=string.match(_weapon, "weapons.bombs") + local _rockets=string.match(_weapon, "weapons.nurs") local _missiles=string.match(_weapon, "weapons.missiles") or _viggen - + -- Check if any condition applies here. local _track = (_bombs and self.trackbombs) or (_rockets and self.trackrockets) or (_missiles and self.trackmissiles) - + -- Get unit name. local _unitName = EventData.IniUnitName - + -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) -- Set this to larger value than the threshold. local dPR=self.BombtrackThreshold*2 - - -- Distance player to range. + + -- Distance player to range. if _unit and _playername then dPR=_unit:GetCoordinate():Get2DDistance(self.location) self:T(RANGE.id..string.format("Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR/1000)) @@ -1128,10 +1128,10 @@ function RANGE:OnEventShot(EventData) -- Tracking info and init of last bomb position. self:T(RANGE.id..string.format("RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName())) - + -- Init bomb position. local _lastBombPos = {x=0,y=0,z=0} - + -- Function monitoring the position of a bomb until impact. local function trackBomb(_ordnance) @@ -1143,38 +1143,38 @@ function RANGE:OnEventShot(EventData) self:T3(RANGE.id..string.format("Range %s: Bomb still in air: %s", self.rangename, tostring(_status))) if _status then - + -- Still in the air. Remember this position. _lastBombPos = {x = _bombPos.x, y = _bombPos.y, z= _bombPos.z } -- Check again in 0.005 seconds. return timer.getTime() + self.dtBombtrack - + else - + -- Bomb did hit the ground. -- Get closet target to last position. local _closetTarget = nil local _distance = nil local _hitquality = "POOR" - + -- Get callsign. local _callsign=self:_myname(_unitName) - + -- Coordinate of impact point. local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) - + -- Check if impact happend in range zone. local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) - + -- Distance from range. We dont want to smoke targets outside of the range. local impactdist=impactcoord:Get2DDistance(self.location) - + -- Impact point of bomb. if self.Debug then impactcoord:MarkToAll("Bomb impact point") end - + -- Smoke impact point of bomb. if self.PlayerSettings[_playername].smokebombimpact and insidezone then if self.PlayerSettings[_playername].delaysmoke then @@ -1183,17 +1183,19 @@ function RANGE:OnEventShot(EventData) impactcoord:Smoke(self.PlayerSettings[_playername].smokecolor) end end - + -- Loop over defined bombing targets. for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - + if _target and _target:IsAlive() then - + -- Distance between bomb and target. local _temp = impactcoord:Get2DDistance(_target:GetCoordinate()) - + + --env.info(string.format("FF target = %s dist = %d m", _target:GetName(), _temp)) + -- Find closest target to last known position of the bomb. if _distance == nil or _temp < _distance then _distance = _temp @@ -1207,7 +1209,7 @@ function RANGE:OnEventShot(EventData) else _hitquality = "POOR" end - + end end end @@ -1222,7 +1224,7 @@ function RANGE:OnEventShot(EventData) -- Local results. local _results = self.bombPlayerResults[_playername] - + -- Add to table. table.insert(_results, {name=_closetTarget.name, distance =_distance, weapon = _weaponName, quality=_hitquality }) @@ -1234,23 +1236,23 @@ function RANGE:OnEventShot(EventData) elseif insidezone then -- Send message local _message=string.format("%s, weapon fell more than %.1f km away from nearest range target. No score!", _callsign, self.scorebombdistance/1000) - self:_DisplayMessageToGroup(_unit, _message, nil, true) + self:_DisplayMessageToGroup(_unit, _message, nil, false) end - + --Terminate the timer self:T(RANGE.id..string.format("Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername)) return nil end -- _status check - + end -- end function trackBomb -- Weapon is not yet "alife" just yet. Start timer in one second. self:T(RANGE.id..string.format("Range %s, player %s: Tracking of weapon starts in one second.", self.rangename, _playername)) timer.scheduleFunction(trackBomb, EventData.weapon, timer.getTime() + 1.0) - + end --if _track (string.match) and player-range distance < threshold. - + end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1267,57 +1269,57 @@ end -- @param #string _unitName Name of the player unit. function RANGE:_DisplayMyStrafePitResults(_unitName) self:F(_unitName) - + -- Get player unit and name local _unit,_playername = self:_GetPlayerUnitAndName(_unitName) - + if _unit and _playername then - + -- Message header. local _message = string.format("My Top %d Strafe Pit Results:\n", self.ndisplayresult) - + -- Get player results. local _results = self.strafePlayerResults[_playername] - + -- Create message. if _results == nil then -- No score yet. _message = string.format("%s: No Score yet.", _playername) else - + -- Sort results table wrt number of hits. local _sort = function( a,b ) return a.hits > b.hits end table.sort(_results,_sort) - + -- Prepare message of best results. local _bestMsg = "" local _count = 1 - + -- Loop over results for _,_result in pairs(_results) do - + -- Message text. _message = _message..string.format("\n[%d] Hits %d - %s - %s", _count, _result.hits, _result.zone.name, _result.text) - + -- Best result. - if _bestMsg == "" then + if _bestMsg == "" then _bestMsg = string.format("Hits %d - %s - %s", _result.hits, _result.zone.name, _result.text) end - + -- 10 runs if _count == self.ndisplayresult then break end - + -- Increase counter _count = _count+1 end - + -- Message text. _message = _message .."\n\nBEST: ".._bestMsg end - -- Send message to group. + -- Send message to group. self:_DisplayMessageToGroup(_unit, _message, nil, true) end end @@ -1327,52 +1329,52 @@ end -- @param #string _unitName Name fo the player unit. function RANGE:_DisplayStrafePitResults(_unitName) self:F(_unitName) - + -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - + -- Check if we have a unit which is a player. if _unit and _playername then - + -- Results table. local _playerResults = {} - + -- Message text. local _message = string.format("Strafe Pit Results - Top %d Players:\n", self.ndisplayresult) - + -- Loop over player results. for _playerName,_results in pairs(self.strafePlayerResults) do - + -- Get the best result of the player. local _best = nil - for _,_result in pairs(_results) do + for _,_result in pairs(_results) do if _best == nil or _result.hits > _best.hits then _best = _result end end - - -- Add best result to table. + + -- Add best result to table. if _best ~= nil then local text=string.format("%s: Hits %i - %s - %s", _playerName, _best.hits, _best.zone.name, _best.text) table.insert(_playerResults,{msg = text, hits = _best.hits}) end - + end - + --Sort list! local _sort = function( a,b ) return a.hits > b.hits end table.sort(_playerResults,_sort) - + -- Add top 10 results. for _i = 1, math.min(#_playerResults, self.ndisplayresult) do _message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg) end - + -- In case there are no scores yet. if #_playerResults<1 then _message = _message.."No player scored yet." end - + -- Send message. self:_DisplayMessageToGroup(_unit, _message, nil, true) end @@ -1384,52 +1386,52 @@ end function RANGE:_DisplayMyBombingResults(_unitName) self:F(_unitName) - -- Get player unit and name. + -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - + if _unit and _playername then - + -- Init message. local _message = string.format("My Top %d Bombing Results:\n", self.ndisplayresult) - + -- Results from player. local _results = self.bombPlayerResults[_playername] - + -- No score so far. if _results == nil then _message = _playername..": No Score yet." else - + -- Sort results wrt to distance. local _sort = function( a,b ) return a.distance < b.distance end table.sort(_results,_sort) - + -- Loop over results. local _bestMsg = "" local _count = 1 for _,_result in pairs(_results) do - + -- Message with name, weapon and distance. _message = _message.."\n"..string.format("[%d] %d m - %s - %s - %s hit", _count, _result.distance, _result.name, _result.weapon, _result.quality) - + -- Store best/first result. if _bestMsg == "" then _bestMsg = string.format("%d m - %s - %s - %s hit",_result.distance,_result.name,_result.weapon, _result.quality) end - + -- Best 10 runs only. if _count == self.ndisplayresult then break end - + -- Increase counter. _count = _count+1 end - + -- Message. _message = _message .."\n\nBEST: ".._bestMsg end - + -- Send message. self:_DisplayMessageToGroup(_unit, _message, nil, true) end @@ -1440,22 +1442,22 @@ end -- @param #string _unitName Name of player unit. function RANGE:_DisplayBombingResults(_unitName) self:F(_unitName) - + -- Results table. local _playerResults = {} - + -- Get player unit and name. local _unit, _player = self:_GetPlayerUnitAndName(_unitName) - + -- Check if we have a unit with a player. if _unit and _player then - + -- Message header. local _message = string.format("Bombing Results - Top %d Players:\n", self.ndisplayresult) - + -- Loop over players. for _playerName,_results in pairs(self.bombPlayerResults) do - + -- Find best result of player. local _best = nil for _,_result in pairs(_results) do @@ -1463,29 +1465,29 @@ function RANGE:_DisplayBombingResults(_unitName) _best = _result end end - + -- Put best result of player into table. if _best ~= nil then local bestres=string.format("%s: %d m - %s - %s - %s hit", _playerName, _best.distance, _best.name, _best.weapon, _best.quality) table.insert(_playerResults, {msg = bestres, distance = _best.distance}) end - + end - + -- Sort list of player results. local _sort = function( a,b ) return a.distance < b.distance end table.sort(_playerResults,_sort) - + -- Loop over player results. - for _i = 1, math.min(#_playerResults, self.ndisplayresult) do + for _i = 1, math.min(#_playerResults, self.ndisplayresult) do _message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg) end - + -- In case there are no scores yet. if #_playerResults<1 then _message = _message.."No player scored yet." end - + -- Send message. self:_DisplayMessageToGroup(_unit, _message, nil, true) end @@ -1499,28 +1501,28 @@ function RANGE:_DisplayRangeInfo(_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=unit:GetCoordinate() - + if self.location then - + -- Direction vector from current position (coord) to target (position). local position=self.location --Core.Point#COORDINATE local rangealt=position:GetLandHeight() local vec3=coord:GetDirectionVec3(position) local angle=coord:GetAngleDegrees(vec3) local range=coord:Get2DDistance(position) - + -- Bearing string. local Bs=string.format('%03d°', angle) - + local texthit if self.PlayerSettings[playername].flaredirecthits then texthit=string.format("Flare direct hits: ON (flare color %s)\n", self:_flarecolor2text(self.PlayerSettings[playername].flarecolor)) @@ -1539,7 +1541,7 @@ function RANGE:_DisplayRangeInfo(_unitname) else textdelay=string.format("Smoke bomb delay: OFF") end - + -- Player unit settings. local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS local trange=string.format("%.1f km", range/1000) @@ -1550,7 +1552,7 @@ function RANGE:_DisplayRangeInfo(_unitname) trangealt=string.format("%d feet", UTILS.MetersToFeet(rangealt)) tstrafemaxalt=string.format("%d feet", UTILS.MetersToFeet(self.strafemaxalt)) end - + -- Message. text=text..string.format("Information on %s:\n", self.rangename) text=text..string.format("-------------------------------------------------------\n") @@ -1562,10 +1564,10 @@ function RANGE:_DisplayRangeInfo(_unitname) text=text..texthit text=text..textbomb text=text..textdelay - + -- Send message to player group. self:_DisplayMessageToGroup(unit, text, nil, true) - + -- Debug output. self:T2(RANGE.id..text) end @@ -1580,27 +1582,27 @@ function RANGE:_DisplayBombTargets(_unitname) -- Get player unit and player name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitname) - + -- Check if we have a player. if _unit and _playername then - + -- Player settings. local _settings=_DATABASE:GetPlayerSettings(_playername) or _SETTINGS --Core.Settings#SETTINGS - + -- Message text. local _text="Bomb Target Locations:" - + for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE if _target and _target:IsAlive() then - + -- Core.Point#COORDINATE local coord=_target:GetCoordinate() --Core.Point#COORDINATE local mycoord=coord:ToStringA2G(_unit, _settings) _text=_text..string.format("\n- %s: %s",_bombtarget.name, mycoord) end end - + self:_DisplayMessageToGroup(_unit,_text, nil, true) end end @@ -1613,23 +1615,23 @@ function RANGE:_DisplayStrafePits(_unitname) -- Get player unit and player name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitname) - + -- Check if we have a player. if _unit and _playername then - + -- Player settings. local _settings=_DATABASE:GetPlayerSettings(_playername) or _SETTINGS --Core.Settings#SETTINGS - + -- Message text. local _text="Strafe Target Locations:" - + for _,_strafepit in pairs(self.strafeTargets) do local _target=_strafepit --Wrapper.Positionable#POSITIONABLE - + -- Pit parameters. local coord=_strafepit.coordinate --Core.Point#COORDINATE local heading=_strafepit.heading - + -- Turn heading around ==> approach heading. if heading>180 then heading=heading-180 @@ -1640,7 +1642,7 @@ function RANGE:_DisplayStrafePits(_unitname) local mycoord=coord:ToStringA2G(_unit, _settings) _text=_text..string.format("\n- %s: %s - heading %03d",_strafepit.name, mycoord, heading) end - + self:_DisplayMessageToGroup(_unit,_text, nil, true) end end @@ -1654,33 +1656,33 @@ function RANGE:_DisplayRangeWeather(_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=unit:GetCoordinate() - + if self.location then - + -- 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 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) @@ -1688,10 +1690,10 @@ function RANGE:_DisplayRangeWeather(_unitname) 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) + 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") @@ -1701,15 +1703,15 @@ function RANGE:_DisplayRangeWeather(_unitname) else text=string.format("No range location defined for range %s.", self.rangename) end - + -- Send message to player group. self:_DisplayMessageToGroup(unit, text, nil, true) - + -- Debug output. self:T2(RANGE.id..text) else self:T(RANGE.id..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) - end + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1733,56 +1735,56 @@ function RANGE:_CheckInZone(_unitName) local _currentStrafeRun = self.strafeStatus[_unitID] if _currentStrafeRun then -- player has already registered for a strafing run. - + -- Get the current approach zone and check if player is inside. local zone=_currentStrafeRun.zone.polygon --Core.Zone#ZONE_POLYGON_BASE - + local unitheading = _unit:GetHeading() local pitheading = _currentStrafeRun.zone.heading - 180 local deltaheading = unitheading-pitheading local towardspit = math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90 - local unitalt=_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight() - + local unitalt=_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight() + -- Check if unit is inside zone and below max height AGL. local unitinzone=_unit:IsInZone(zone) and unitalt <= self.strafemaxalt and towardspit - + -- Debug output local text=string.format("Checking stil in zone. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _unitName, _playername, tostring(unitinzone), unitalt, deltaheading) self:T2(RANGE.id..text) - + -- Check if player is in strafe zone and below max alt. - if unitinzone then - + if unitinzone then + -- Still in zone, keep counting hits. Increase counter. _currentStrafeRun.time = _currentStrafeRun.time+1 - + else - + -- Increase counter _currentStrafeRun.time = _currentStrafeRun.time+1 - + if _currentStrafeRun.time <= 3 then - + -- Reset current run. self.strafeStatus[_unitID] = nil - + -- Message text. local _msg = string.format("%s left strafing zone %s too quickly. No Score.", _playername, _currentStrafeRun.zone.name) - + -- Send message. self:_DisplayMessageToGroup(_unit, _msg, nil, true) - + else - + -- Get current ammo. local _ammo=self:_GetAmmo(_unitName) - + -- Result. local _result = self.strafeStatus[_unitID] -- Judge this pass. Text is displayed on summary. if _result.hits >= _result.zone.goodPass*2 then - _result.text = "EXCELLENT PASS" + _result.text = "EXCELLENT PASS" elseif _result.hits >= _result.zone.goodPass then _result.text = "GOOD PASS" elseif _result.hits >= _result.zone.goodPass/2 then @@ -1790,81 +1792,81 @@ function RANGE:_CheckInZone(_unitName) else _result.text = "POOR PASS" end - + -- Calculate accuracy of run. Number of hits wrt number of rounds fired. local shots=_result.ammo-_ammo local accur=0 if shots>0 then accur=_result.hits/shots*100 end - - -- Message text. + + -- Message text. local _text=string.format("%s, %s with %d hits on target %s.", self:_myname(_unitName), _result.text, _result.hits, _result.zone.name) if shots and accur then _text=_text..string.format("\nTotal rounds fired %d. Accuracy %.1f %%.", shots, accur) end - + -- Send message. self:_DisplayMessageToGroup(_unit, _text) - + -- Set strafe status to nil. self.strafeStatus[_unitID] = nil - + -- Save stats so the player can retrieve them. local _stats = self.strafePlayerResults[_playername] or {} table.insert(_stats, _result) self.strafePlayerResults[_playername] = _stats end - + end else - + -- Check to see if we're in any of the strafing zones (first time). for _,_targetZone in pairs(self.strafeTargets) do - + -- Get the current approach zone and check if player is inside. local zonenname=_targetZone.name local zone=_targetZone.polygon --Core.Zone#ZONE_POLYGON_BASE - + -- Check if player is in zone and below max alt and flying towards the target. local unitheading = _unit:GetHeading() local pitheading = _targetZone.heading - 180 local deltaheading = unitheading-pitheading local towardspit = math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90 - local unitalt =_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight() - + local unitalt =_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight() + -- Check if unit is inside zone and below max height AGL. local unitinzone=_unit:IsInZone(zone) and unitalt <= self.strafemaxalt and towardspit - + -- Debug info. local text=string.format("Checking zone %s. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _targetZone.name, _unitName, _playername, tostring(unitinzone), unitalt, deltaheading) self:T2(RANGE.id..text) - + -- Player is inside zone. if unitinzone then - + -- Get ammo at the beginning of the run. local _ammo=self:_GetAmmo(_unitName) -- Init strafe status for this player. self.strafeStatus[_unitID] = {hits = 0, zone = _targetZone, time = 1, ammo=_ammo, pastfoulline=false } - + -- Rolling in! local _msg=string.format("%s, rolling in on strafe pit %s.", self:_myname(_unitName), _targetZone.name) - + -- Send message. self:_DisplayMessageToGroup(_unit, _msg, 10, true) -- We found our player. Skip remaining checks. break - - end -- unit in zone check - + + end -- unit in zone check + end -- loop over zones end end - + end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1875,24 +1877,24 @@ end -- @param #string _unitName Name of player unit. function RANGE:_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.MenuAddedTo[_gid] then - + -- Enable switch so we don't do this twice. self.MenuAddedTo[_gid] = true - + -- Main F10 menu: F10/On the Range// if RANGE.MenuF10[_gid] == nil then RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") @@ -1908,8 +1910,8 @@ function RANGE:_AddF10Commands(_unitName) -- F10/On the Range//Mark Targets/ missionCommands.addCommandForGroup(_gid, "Mark On Map", _markPath, self._MarkTargetsOnMap, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Illuminate Range", _markPath, self._IlluminateBombTargets, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Smoke Strafe Pits", _markPath, self._SmokeStrafeTargetBoxes, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Illuminate Range", _markPath, self._IlluminateBombTargets, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Smoke Strafe Pits", _markPath, self._SmokeStrafeTargetBoxes, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Strafe Tgts", _markPath, self._SmokeStrafeTargets, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Bomb Tgts", _markPath, self._SmokeBombTargets, self, _unitName) -- F10/On the Range//Stats/ @@ -1932,7 +1934,7 @@ function RANGE:_AddF10Commands(_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) + 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._DisplayRangeWeather, self, _unitName) @@ -1947,7 +1949,7 @@ function RANGE:_AddF10Commands(_unitName) end end - + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Helper Functions @@ -1957,35 +1959,35 @@ end -- @return Number of shells left function RANGE:_GetAmmo(unitname) self:F2(unitname) - + -- Init counter. local ammo=0 - + local unit, playername = self:_GetPlayerUnitAndName(unitname) - + if unit and playername then - + local has_ammo=false - + local ammotable=unit:GetAmmo() self:T2({ammotable=ammotable}) - + if ammotable ~= nil then - + local weapons=#ammotable self:T2(RANGE.id..string.format("Number of weapons %d.", weapons)) - + for w=1,weapons do - + local Nammo=ammotable[w]["count"] local Tammo=ammotable[w]["desc"]["typeName"] - + -- We are specifically looking for shells here. if string.match(Tammo, "shell") then - + -- Add up all shells ammo=ammo+Nammo - + local text=string.format("Player %s has %d rounds ammo of type %s", playername, Nammo, Tammo) self:T(RANGE.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) @@ -1997,7 +1999,7 @@ function RANGE:_GetAmmo(unitname) end end end - + return ammo end @@ -2012,7 +2014,7 @@ function RANGE:_MarkTargetsOnMap(_unitName) if _unitName then group=UNIT:FindByName(_unitName):GetGroup() end - + -- Mark bomb targets. for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE @@ -2025,7 +2027,7 @@ function RANGE:_MarkTargetsOnMap(_unitName) end end end - + -- Mark strafe targets. for _,_strafepit in pairs(self.strafeTargets) do for _,_target in pairs(_strafepit.targets) do @@ -2040,13 +2042,13 @@ function RANGE:_MarkTargetsOnMap(_unitName) end end end - + if _unitName then local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) local text=string.format("%s, %s, range targets are now marked on F10 map.", self.rangename, _playername) self:_DisplayMessageToGroup(_unit, text, 5) end - + end --- Illuminate targets. Fires illumination bombs at one random bomb and one random strafe target at a random altitude between 400 and 800 m. @@ -2065,16 +2067,16 @@ function RANGE:_IlluminateBombTargets(_unitName) table.insert(bomb, coord) end end - + if #bomb>0 then local coord=bomb[math.random(#bomb)] --Core.Point#COORDINATE local c=COORDINATE:New(coord.x,coord.y+math.random(self.illuminationminalt,self.illuminationmaxalt),coord.z) c:IlluminationBomb() end - + -- All strafe target coordinates. local strafe={} - + for _,_strafepit in pairs(self.strafeTargets) do for _,_target in pairs(_strafepit.targets) do local _target=_target --Wrapper.Positionable#POSITIONABLE @@ -2084,14 +2086,14 @@ function RANGE:_IlluminateBombTargets(_unitName) end end end - + -- Pick a random strafe target. if #strafe>0 then local coord=strafe[math.random(#strafe)] --Core.Point#COORDINATE local c=COORDINATE:New(coord.x,coord.y+math.random(self.illuminationminalt,self.illuminationmaxalt),coord.z) c:IlluminationBomb() end - + if _unitName then local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) local text=string.format("%s, %s, range targets are illuminated.", self.rangename, _playername) @@ -2105,10 +2107,10 @@ end function RANGE:_ResetRangeStats(_unitName) self:F(_unitName) - -- Get player unit and name. + -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - - if _unit and _playername then + + if _unit and _playername then self.strafePlayerResults[_playername] = nil self.bombPlayerResults[_playername] = nil local text=string.format("%s, %s, your range stats were cleared.", self.rangename, _playername) @@ -2124,15 +2126,15 @@ end -- @param #boolean _clear Clear up old messages. function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear) self:F({unit=_unit, text=_text, time=_time, clear=_clear}) - + _time=_time or self.Tmsg if _clear==nil then _clear=false end - + -- Group ID. local _gid=_unit:GetGroup():GetID() - + if _gid and not self.examinerexclusive then if _clear == true then trigger.action.outTextForGroup(_gid, _text, _time, _clear) @@ -2149,9 +2151,9 @@ function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear) else trigger.action.outTextForGroup(_examinerid, _text, _time) end - end + end end - + end --- Toggle status of smoking bomb impact points. @@ -2159,7 +2161,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeBombImpactOnOff(unitname) self:F(unitname) - + local unit, playername = self:_GetPlayerUnitAndName(unitname) if unit and playername then local text @@ -2172,7 +2174,7 @@ function RANGE:_SmokeBombImpactOnOff(unitname) end self:_DisplayMessageToGroup(unit, text, 5) end - + end --- Toggle status of time delay for smoking bomb impact points @@ -2180,7 +2182,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeBombDelayOnOff(unitname) self:F(unitname) - + local unit, playername = self:_GetPlayerUnitAndName(unitname) if unit and playername then local text @@ -2193,7 +2195,7 @@ function RANGE:_SmokeBombDelayOnOff(unitname) end self:_DisplayMessageToGroup(unit, text, 5) end - + end --- Toggle status of flaring direct hits of range targets. @@ -2201,7 +2203,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_FlareDirectHitsOnOff(unitname) self:F(unitname) - + local unit, playername = self:_GetPlayerUnitAndName(unitname) if unit and playername then local text @@ -2214,7 +2216,7 @@ function RANGE:_FlareDirectHitsOnOff(unitname) end self:_DisplayMessageToGroup(unit, text, 5) end - + end --- Mark bombing targets with smoke. @@ -2222,7 +2224,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeBombTargets(unitname) self:F(unitname) - + for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE if _target and _target:IsAlive() then @@ -2230,13 +2232,13 @@ function RANGE:_SmokeBombTargets(unitname) coord:Smoke(self.BombSmokeColor) end end - + if unitname then local unit, playername = self:_GetPlayerUnitAndName(unitname) local text=string.format("%s, %s, bombing targets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.BombSmokeColor)) self:_DisplayMessageToGroup(unit, text, 5) end - + end --- Mark strafing targets with smoke. @@ -2244,17 +2246,17 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeStrafeTargets(unitname) self:F(unitname) - + for _,_target in pairs(self.strafeTargets) do _target.coordinate:Smoke(self.StrafeSmokeColor) end - + if unitname then local unit, playername = self:_GetPlayerUnitAndName(unitname) local text=string.format("%s, %s, strafing tragets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.StrafeSmokeColor)) self:_DisplayMessageToGroup(unit, text, 5) end - + end --- Mark approach boxes of strafe targets with smoke. @@ -2262,7 +2264,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeStrafeTargetBoxes(unitname) self:F(unitname) - + for _,_target in pairs(self.strafeTargets) do local zone=_target.polygon --Core.Zone#ZONE zone:SmokeZone(self.StrafePitSmokeColor) @@ -2270,13 +2272,13 @@ function RANGE:_SmokeStrafeTargetBoxes(unitname) _point:SmokeOrange() --Corners are smoked orange. end end - + if unitname then local unit, playername = self:_GetPlayerUnitAndName(unitname) local text=string.format("%s, %s, strafing pit approach boxes are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.StrafePitSmokeColor)) self:_DisplayMessageToGroup(unit, text, 5) end - + end --- Sets the smoke color used to smoke players bomb impact points. @@ -2285,14 +2287,14 @@ end -- @param Utilities.Utils#SMOKECOLOR color ID of the smoke color. function RANGE:_playersmokecolor(_unitName, color) self:F({unitname=_unitName, color=color}) - + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) if _unit and _playername then self.PlayerSettings[_playername].smokecolor=color local text=string.format("%s, %s, your bomb impacts are now smoked in %s.", self.rangename, _playername, self:_smokecolor2text(color)) self:_DisplayMessageToGroup(_unit, text, 5) end - + end --- Sets the flare color used when player makes a direct hit on target. @@ -2301,14 +2303,14 @@ end -- @param Utilities.Utils#FLARECOLOR color ID of flare color. function RANGE:_playerflarecolor(_unitName, color) self:F({unitname=_unitName, color=color}) - + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) if _unit and _playername then self.PlayerSettings[_playername].flarecolor=color local text=string.format("%s, %s, your direct hits are now flared in %s.", self.rangename, _playername, self:_flarecolor2text(color)) self:_DisplayMessageToGroup(_unit, text, 5) end - + end --- Converts a smoke color id to text. E.g. SMOKECOLOR.Blue --> "blue". @@ -2317,7 +2319,7 @@ end -- @return #string Color text. function RANGE:_smokecolor2text(color) self:F(color) - + local txt="" if color==SMOKECOLOR.Blue then txt="blue" @@ -2332,7 +2334,7 @@ function RANGE:_smokecolor2text(color) else txt=string.format("unkown color (%s)", tostring(color)) end - + return txt end @@ -2342,7 +2344,7 @@ end -- @return #string Color text. function RANGE:_flarecolor2text(color) self:F(color) - + local txt="" if color==FLARECOLOR.Green then txt="green" @@ -2355,7 +2357,7 @@ function RANGE:_flarecolor2text(color) else txt=string.format("unkown color (%s)", tostring(color)) end - + return txt end @@ -2368,23 +2370,23 @@ function RANGE:_CheckStatic(name) -- Get DCS static object. local _DCSstatic=StaticObject.getByName(name) - + if _DCSstatic and _DCSstatic:isExist() then - + --Static does exist at least in DCS. Check if it also in the MOOSE DB. local _MOOSEstatic=STATIC:FindByName(name, false) - + -- If static is not yet in MOOSE DB, we add it. Can happen for cargo statics! if not _MOOSEstatic then self:T(RANGE.id..string.format("Adding DCS static to MOOSE database. Name = %s.", name)) _DATABASE:AddStatic(name) end - + return true else self:T3(RANGE.id..string.format("No static object with name %s exists.", name)) end - + -- Check if a unit has this name. if UNIT:FindByName(name) then return false @@ -2405,18 +2407,18 @@ function RANGE:_GetSpeed(controllable) -- Get DCS descriptors local desc=controllable:GetDesc() - + -- Get speed local speed=0 if desc then speed=desc.speedMax*3.6 self:T({speed=speed}) end - + return speed end ---- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. -- @param #RANGE self -- @param #string _unitName Name of the player unit. -- @return Wrapper.Unit#UNIT Unit of player. @@ -2426,38 +2428,38 @@ function RANGE:_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 ---- Returns a string which consits of this callsign and the player name. +--- Returns a string which consits of this callsign and the player name. -- @param #RANGE self -- @param #string unitname Name of the player unit. function RANGE:_myname(unitname) self:F2(unitname) - + local unit=UNIT:FindByName(unitname) local pname=unit:GetPlayerName() local csign=unit:GetCallsign() - + return string.format("%s (%s)", csign, pname) end @@ -2468,13 +2470,13 @@ end -- @return #table Split text. function RANGE:_split(str, sep) self:F2({str=str, sep=sep}) - + local result = {} local regex = ("([^%s]+)"):format(sep) for each in str:gmatch(regex) do table.insert(result, each) end - + return result end diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 9222f6321..f65037e6c 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! @@ -624,7 +625,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. @@ -1555,6 +1557,7 @@ WAREHOUSE = { autosave = false, autosavepath = nil, autosavefile = nil, + saveparking = false, } --- Item of the warehouse stock table. @@ -1726,7 +1729,7 @@ WAREHOUSE.db = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.4" +WAREHOUSE.version="0.6.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1735,12 +1738,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. @@ -1866,7 +1869,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! @@ -2367,6 +2370,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. @@ -3534,12 +3555,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 @@ -3592,6 +3613,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. @@ -4624,7 +4646,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 @@ -6300,25 +6322,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 @@ -6456,7 +6479,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) @@ -6475,6 +6498,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. @@ -6916,13 +6940,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 @@ -6932,6 +6956,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 @@ -7006,20 +7039,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. @@ -7030,7 +7049,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]={} @@ -7052,10 +7071,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. diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 61625bc4f..02f83c679 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -545,10 +545,6 @@ do -- ZONE_CAPTURE_COALITION -- @param #ZONE_CAPTURE_COALITION self -- @param #number Delay - -- We check if a unit within the zone is hit. - -- If it is, then we must move the zone to attack state. - self:HandleEvent( EVENTS.Hit, self.OnEventHit ) - return self end @@ -793,20 +789,5 @@ do -- ZONE_CAPTURE_COALITION end end - --- @param #ZONE_CAPTURE_COALITION self - -- @param Core.Event#EVENTDATA EventData The event data. - function ZONE_CAPTURE_COALITION:OnEventHit( EventData ) - - local UnitHit = EventData.TgtUnit - - if UnitHit then - if UnitHit:IsInZone( self.Zone ) then - self:Attack() - end - end - - end - - end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 8b85b0948..91bc437a4 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -993,6 +993,38 @@ 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 + + local P1=Coord:GetVec2() + local P2=nil + if CoordRaceTrack then + Pattern=AI.Task.OrbitPattern.RACE_TRACK + P2=CoordRaceTrack:GetVec2() + end + + local Task = { + id = 'Orbit', + params = { + pattern = Pattern, + point = P1, + point2 = P2, + 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. @@ -1081,11 +1113,7 @@ function CONTROLLABLE:TaskRefueling() -- params = {} -- } - local DCSTask - DCSTask = { id = 'Refueling', - params = { - }, - }, + local DCSTask={id='Refueling', params={}} self:T3( { DCSTask } ) return DCSTask @@ -2224,7 +2252,7 @@ do -- Route methods FromCoordinate = FromCoordinate or self:GetCoordinate() -- Get path and path length on road including the end points (From and To). - local PathOnRoad, LengthOnRoad=FromCoordinate:GetPathOnRoad(ToCoordinate, true) + local PathOnRoad, LengthOnRoad, GotPath =FromCoordinate:GetPathOnRoad(ToCoordinate, true) -- Get the length only(!) on the road. local _,LengthRoad=FromCoordinate:GetPathOnRoad(ToCoordinate, false) @@ -2236,7 +2264,7 @@ do -- Route methods -- Calculate the direct distance between the initial and final points. local LengthDirect=FromCoordinate:Get2DDistance(ToCoordinate) - if PathOnRoad then + if GotPath then -- Off road part of the rout: Total=OffRoad+OnRoad. LengthOffRoad=LengthOnRoad-LengthRoad @@ -2259,7 +2287,7 @@ do -- Route methods local canroad=false -- Check if a valid path on road could be found. - if PathOnRoad and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. + if GotPath and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. -- Check whether the road is very long compared to direct path. if LongRoad and Shortcut then @@ -3147,6 +3175,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 9b827ae61..c93866343 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/Static.lua b/Moose Development/Moose/Wrapper/Static.lua index fb3d73296..e624dc021 100644 --- a/Moose Development/Moose/Wrapper/Static.lua +++ b/Moose Development/Moose/Wrapper/Static.lua @@ -213,36 +213,3 @@ function STATIC:ReSpawnAt( Coordinate, Heading ) SpawnStatic:ReSpawnAt( Coordinate, Heading ) end - - ---- Returns true if the unit is within a @{Zone}. --- @param #STATIC self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is within the @{Core.Zone#ZONE_BASE} -function STATIC:IsInZone( Zone ) - self:F2( { self.StaticName, Zone } ) - - if self:IsAlive() then - local IsInZone = Zone:IsVec3InZone( self:GetVec3() ) - - return IsInZone - end - return false -end - ---- Returns true if the unit is not within a @{Zone}. --- @param #STATIC self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is not within the @{Core.Zone#ZONE_BASE} -function STATIC:IsNotInZone( Zone ) - self:F2( { self.StaticName, Zone } ) - - if self:IsAlive() then - local IsInZone = not Zone:IsVec3InZone( self:GetVec3() ) - - self:T( { IsInZone } ) - return IsInZone - else - return false - end -end diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index d24a0b0b0..d81a6c01c 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -902,29 +902,31 @@ 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 = true + -- Get DCS result of whether unit is in air or not. + local UnitInAir = DCSUnit:inAir() + + -- Get unit category. local UnitCategory = DCSUnit:getDesc().category - if UnitCategory == Unit.Category.HELICOPTER then + + -- 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. + -- This is a workaround since DCS currently does not acknoledge that helos land on buildings. + -- Note however, that the velocity check will fail if the ground is moving, e.g. on an aircraft carrier! + if UnitInAir==true and UnitCategory == Unit.Category.HELICOPTER 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 - else - UnitInAir = DCSUnit:inAir() end - - + self:T3( UnitInAir ) return UnitInAir end diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 231861fe0..9ef0e3f57 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -60,16 +60,11 @@ Functional/PseudoATC.lua Functional/Warehouse.lua AI/AI_Balancer.lua -AI/AI_Air.lua AI/AI_A2A.lua AI/AI_A2A_Patrol.lua AI/AI_A2A_Cap.lua AI/AI_A2A_Gci.lua AI/AI_A2A_Dispatcher.lua -AI/AI_A2G.lua -AI/AI_A2G_Engage.lua -AI/AI_A2G_Patrol.lua -AI/AI_A2G_Dispatcher.lua AI/AI_Patrol.lua AI/AI_Cap.lua AI/AI_Cas.lua From 3d54d78be86d7268d48c56b6ef0273c56b30d4ba Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 27 Nov 2018 23:37:48 +0100 Subject: [PATCH 063/147] AIRBOSS v0.3.7 Zones. --- Moose Development/Moose/Ops/Airboss.lua | 430 +++++++++++++++++++----- 1 file changed, 354 insertions(+), 76 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index dfbb9bb0d..3545bebcc 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -48,9 +48,6 @@ -- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneInitial Zone usually 3 NM astern of carrier where pilots start their CASE I pattern. --- @field Core.Zone#ZONE_UNIT zonePlatform Zone astern the carrier where pilots should hit 5000 ft in CASE II/III. --- @field Core.Zone#ZONE_UNIT zoneDirtyup Zone astern the carrier where pilots should hit 1200 ft and dirty up. --- @field Core.Zone#ZONE_UNIT zoneBullseye Zone astern the carrier where pilots should intercept the glide slope. -- @field #table players Table of players. -- @field #table menuadded Table of units where the F10 radio menu was added. -- @field #AIRBOSS.Checkpoint Upwind Upwind checkpoint. @@ -119,9 +116,6 @@ AIRBOSS = { zoneCCA = nil, zoneCCZ = nil, zoneInitial = nil, - zonePlatform = nil, - zoneDirtyup = nil, - zoneBullseye = nil, players = {}, menuadded = {}, Upwind = {}, @@ -454,7 +448,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.6" +AIRBOSS.version="0.3.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -583,14 +577,6 @@ function AIRBOSS:New(carriername, alias) -- CASE I/II moving zone: Zone 3 NM astern and 100 m starboard of the carrier with radius of 0.5 km. self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, 0.5*1000, {dx=-UTILS.NMToMeters(3), dy=100, relative_to_unit=true}) - -- CASE II/III moving zones. - local radial=180+self.carrierparam.rwyangle - local radius=UTILS.NMToMeters(1) - self.zonePlatform = ZONE_UNIT:New("Platform Zone", self.carrier, radius, {rho=UTILS.NMToMeters(19), theta=radial+self.holdingoffset, relative_to_unit=true}) - self.zoneArcturn1 = ZONE_UNIT:New("ArcTurn1 Zone", self.carrier, radius, {rho=UTILS.NMToMeters(14), theta=radial+self.holdingoffset, relative_to_unit=true}) - self.zoneArcturn2 = ZONE_UNIT:New("ArcTurn2 Zone", self.carrier, radius, {rho=UTILS.NMToMeters(12), theta=radial, relative_to_unit=true}) - self.zoneDirtyup = ZONE_UNIT:New("DirtyUp Zone", self.carrier, radius, {rho=UTILS.NMToMeters( 9), theta=radial, relative_to_unit=true}) - self.zoneBullseye = ZONE_UNIT:New("Bulleye Zone", self.carrier, radius, {rho=UTILS.NMToMeters( 3), theta=radial, relative_to_unit=true}) -- Smoke zones. if self.Debug then @@ -598,7 +584,15 @@ function AIRBOSS:New(carriername, alias) --self.zonePlatform:SmokeZone(SMOKECOLOR.Orange, 90) --self.zoneDirtyup:SmokeZone(SMOKECOLOR.Blue, 90) --self.zoneBullseye:SmokeZone(SMOKECOLOR.Red, 90) - --local zp=self:_GetCase23ValidZone():SmokeZone(SMOKECOLOR.Green, 45) + --self.zoneInitial:SmokeZone(SMOKECOLOR.White, 90) + local case=3 + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + end -- Init default sound files. @@ -626,6 +620,7 @@ function AIRBOSS:New(carriername, alias) self:AddTransition("*", "Idle", "Idle") -- Carrier is idleing. self:AddTransition("Idle", "Recover", "Recovering") -- Recover aircraft. self:AddTransition("*", "Status", "*") -- Update status of players and queues. + self:AddTransition("*", "Case", "*") -- Switch to another case recovery. self:AddTransition("*", "Stop", "Stopped") -- Stop AIRBOSS script. @@ -659,6 +654,20 @@ function AIRBOSS:New(carriername, alias) -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Case" that switches the recovery case. + -- @function [parent=#AIRBOSS] Case + -- @param #AIRBOSS self + -- @param #number OldCase Old recovery case. + -- @param #number NewCase New recovery case. + + --- Triggers the delayed FSM event "Case" that switches the recovery case + -- @function [parent=#AIRBOSS] __Case + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number OldCase Old recovery case. + -- @param #number NewCase New recovery case. + + --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. -- @function [parent=#AIRBOSS] Stop -- @param #AIRBOSS self @@ -859,7 +868,7 @@ function AIRBOSS:IsIdle() end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- FSM states +-- FSM event functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. @@ -1011,6 +1020,44 @@ function AIRBOSS:_CheckRecoveryTimes() end +--- On before "Case" event. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number OldCase The old (current) case. +-- @param #number NewCase The new case. +-- @return #boolean If true, switching to new case recovery is allowed. +function AIRBOSS:onbeforeCase(From, Event, To, OldCase, NewCase) + if NewCase==self.case then + -- Old=New ==> no switch necessary + return false + end + + if NewCase<1 or NewCase>3 then + self:E(self.lid.."ERROR: new case is not 1, 2 or 3 but %s", tostring(NewCase)) + return false + end + + return true +end + +--- On after "Case" event. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number OldCase The old (current) case. +-- @param #number NewCase The new case. +function AIRBOSS:onbeforeCase(From, Event, To, OldCase, NewCase) + + self.case=NewCase + + + +end + + --- On before "Recover" event. -- @param #AIRBOSS self -- @param #string From From state. @@ -2632,52 +2679,253 @@ function AIRBOSS:_Holding(playerData) self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 5) end - ---- Get CASE II/III box zone. +--- Get Bullseye zone with radius 1 NM and DME 3 NM from the carrier. Radial depends on recovery case. -- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneBullseye(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 3 NM + local distance=UTILS.NMToMeters(3) + + -- Zone depends on Case recovery. + local radial + if case==2 then + + radial=self:GetRadialCase2(false, false) + + elseif case==3 then + + radial=self:GetRadialCase3(false, false) + + else + + self:E(self.lid.."ERROR: Bullseye zone only for CASE II or III recoveries!") + return nil + + end + + -- Get coordinate and vec2. + local coord=self:GetCoordinate():Translate(distance, radial) + local vec2=coord:GetVec2() + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Bullseye", vec2, radius) + + return zone +end + +--- Get dirty up zone with radius 1 NM and DME 9 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneDirtyUp(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 19 NM + local distance=UTILS.NMToMeters(9) + + -- Zone depends on Case recovery. + local radial + if case==2 then + + radial=self:GetRadialCase2(false, false) + + elseif case==3 then + + radial=self:GetRadialCase3(false, false) + + else + + self:E(self.lid.."ERROR: Dirty Up zone only for CASE II or III recoveries!") + return nil + + end + + -- Get coordinate and vec2. + local coord=self:GetCoordinate():Translate(distance, radial) + local vec2=coord:GetVec2() + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Dirty Up", vec2, radius) + + return zone +end + +--- Get arc out zone with radius 1 NM and DME 12 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneArcOut(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 12 NM + local distance=UTILS.NMToMeters(12) + + -- Zone depends on Case recovery. + local radial + if case==2 then + + radial=self:GetRadialCase2(false, false) + + elseif case==3 then + + radial=self:GetRadialCase3(false, false) + + else + + self:E(self.lid.."ERROR: Arc out zone only for CASE II or III recoveries!") + return nil + + end + + -- Get coordinate and vec2. + local coord=self:GetCoordinate():Translate(distance, radial) + local vec2=coord:GetVec2() + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Arc Out", vec2, radius) + + return zone +end + +--- Get arc in zone with radius 1 NM and DME 14 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneArcIn(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Zone depends on Case recovery. + local radial + if case==2 then + + radial=self:GetRadialCase2(false, true) + + elseif case==3 then + + radial=self:GetRadialCase3(false, true) + + else + + self:E(self.lid.."ERROR: Arc in zone only for CASE II or III recoveries!") + return nil + + end + + -- Distance = 14 NM + local distance=UTILS.NMToMeters(12/math.cos(math.rad(self.holdingoffset))) + + -- Get coordinate and vec2. + local coord=self:GetCoordinate():Translate(distance, radial) + local vec2=coord:GetVec2() + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Arc In", vec2, radius) + + return zone +end + +--- Get platform zone with radius 1 NM and DME 19 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Circular platform zone. +function AIRBOSS:_GetZonePlatform(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 19 NM + local distance=UTILS.NMToMeters(19) + + -- Zone depends on Case recovery. + local radial + if case==2 then + + radial=self:GetRadialCase2(false, true) + + elseif case==3 then + + radial=self:GetRadialCase3(false, true) + + else + + self:E(self.lid.."ERROR: Platform zone only for CASE II or III recoveries!") + return nil + + end + + -- Get coordinate and vec2. + local coord=self:GetCoordinate():Translate(distance, radial) + local vec2=coord:GetVec2() + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Platform", vec2, radius) + + return zone +end + + +--- Get approach corridor zone. Shape depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. -- @return Core.Zone#ZONE_POLYGON_BASE Box zone. -function AIRBOSS:_GetCase23ValidZone() +function AIRBOSS:_GetZoneCorridor(case) - -- Radial. - local hdg=self:GetRadialCase3(false, false) - local off=self:GetRadialCase3(false, true) + -- Radial and offset. + local radial + local offset + -- Select case. + if case==2 then + radial=self:GetRadialCase2(false, false) + offset=self:GetRadialCase2(false, true) + elseif case==3 then + radial=self:GetRadialCase3(false, false) + offset=self:GetRadialCase3(false, true) + else + radial=self:GetRadialCase3(false, false) + offset=self:GetRadialCase3(false, true) + end + + self:I(string.format("FF case %d radial = %d", case, radial)) + self:I(string.format("FF case %d offset = %d", case, offset)) - self:I("FF radial case 3 = "..hdg) - -- Width of the box. local w=UTILS.NMToMeters(5) -- Length of the box. - local l=UTILS.NMToMeters(50) - - -- Coordinate of the carrier. - local c0=self:GetCoordinate() - - -- Do not jump diagonal because it screws up the smoke zones. - local c1=c0:Translate(w, hdg+90) -- Starboard close - local c2=c1:Translate(l, hdg) -- Starboard far - local c4=c0:Translate(w, hdg-90) -- Port close - local c3=c4:Translate(l, hdg) -- Port far - - local beta=hdg-self.holdingoffset - env.info("FF beta = "..beta) + local l=UTILS.NMToMeters(10) + + local beta=radial-self.holdingoffset + --env.info("FF beta = "..beta) local x=(50-9)/math.cos(math.rad(beta)) - env.info("FF x [NM] = "..x) + --env.info("FF x [NM] = "..x) local c={} c[1]=self:GetCoordinate() - c[2]=c[1]:Translate( UTILS.NMToMeters( 5), hdg-90) - c[3]=c[2]:Translate( UTILS.NMToMeters( 9), hdg) - c[4]=c[3]:Translate( UTILS.NMToMeters( x), -beta) ---[[ - c[5]=c[4]:Translate( UTILS.NMToMeters(10), hdg+90) - c[6]=c[5]:Translate(-UTILS.NMToMeters( x), beta) - c[7]=c[6]:Translate(-UTILS.NMToMeters( 5), hdg-90) - ]] + c[2]=c[1]:Translate( UTILS.NMToMeters( 1), radial-90) + c[3]=c[2]:Translate( UTILS.NMToMeters(13), radial) + c[4]=c[3]:Translate( UTILS.NMToMeters( 2), radial+90) + c[5]=c[4]:Translate( UTILS.NMToMeters(10), offset) + c[6]=c[5]:Translate( UTILS.NMToMeters( 2), radial+90) + c[7]=c[6]:Translate(-UTILS.NMToMeters(10), offset) --This is more difficult! + c[8]=c[7]:Translate( UTILS.NMToMeters( 2), radial-90) + c[9]=c[8]:Translate(-UTILS.NMToMeters(13), radial) + -- Create an array of a square! local p={} for _i,_c in ipairs(c) do + _c:SmokeBlue() p[_i]=_c:GetVec2() end @@ -2694,6 +2942,8 @@ end -- @return Core.Zone#ZONE Holding zone. function AIRBOSS:_GetHoldingZone(playerData) + --TODO: make indepened of whole playerData. Just use case and stack as input! + -- Player unit and flight. local unit=playerData.unit @@ -2724,6 +2974,8 @@ function AIRBOSS:_GetHoldingZone(playerData) -- TODO: Include 15 or 30 degrees offset. local hdg=self.carrier:GetHeading()-self.holdingoffset + + --TODO: case dependend radial! local hdg=self:GetRadialCase3(false) -- Create an array of a square! @@ -2816,7 +3068,7 @@ end function AIRBOSS:_Platform(playerData) -- Check if player is in valid zone - local validzone=self:_GetCase23ValidZone() + local validzone=self:_GetZoneCorridor(playerData.case) -- Check if we are inside the moving zone. local invalid=playerData.unit:IsNotInZone(validzone) @@ -2828,7 +3080,7 @@ function AIRBOSS:_Platform(playerData) end -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self.zonePlatform) + local inzone=playerData.unit:IsInZone(self:_GetZonePlatform(playerData.case)) -- Check if we are in zone. if inzone then @@ -2863,7 +3115,7 @@ end function AIRBOSS:_DirtyUp(playerData) -- Check if player is in valid zone - local validzone=self:_GetCase23ValidZone() + local validzone=self:_GetZoneCorridor(playerData.case) -- Check if we are inside the moving zone. local invalid=playerData.unit:IsNotInZone(validzone) @@ -2875,7 +3127,7 @@ function AIRBOSS:_DirtyUp(playerData) end -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self.zoneDirtyup) + local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) --if self:_CheckLimits(X, Z, self.DirtyUp) then if inzone then @@ -2917,7 +3169,7 @@ end function AIRBOSS:_Bullseye(playerData) -- Check if player is in valid zone - local validzone=self:_GetCase23ValidZone() + local validzone=self:_GetZoneCorridor(playerData.case) -- Check if we are inside the moving zone. local invalid=playerData.unit:IsNotInZone(validzone) @@ -2929,7 +3181,7 @@ function AIRBOSS:_Bullseye(playerData) end -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self.zoneBullseye) + local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) -- Check that we reached the position. --if self:_CheckLimits(X, Z, self.Bullseye) then @@ -3724,14 +3976,37 @@ function AIRBOSS:GetFinalBearing(magnetic) return fb end ---- Get radial, i.e. the final bearing FB-180 degrees including holding offset for Case II/III recoveries. +--- Get radial with respect to carrier heading and (optionally) holding offset. This is used in Case II recoveries. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, magnetic radial is returned. Default is true radial. +-- @param #boolean offset If true, inlcude holding offset. +-- @return #number Radial in degrees. +function AIRBOSS:GetRadialCase2(magnetic, offset) + + -- Radial wrt to heading of carrier. + local radial=self:GetHeading(magnetic)-180 + + -- Holding offset angle (+-15 or 30 degrees usually) + if offset then + radial=radial-self.holdingoffset + end + + -- Adjust for negative values. + if radial<0 then + radial=radial+360 + end + + return radial +end + +--- Get radial with respect to angled runway and (optionally) holding offset. This is used in Case III recoveries. -- @param #AIRBOSS self -- @param #boolean magnetic If true, magnetic radial is returned. Default is true radial. -- @param #boolean offset If true, inlcude holding offset. -- @return #number Radial in degrees. function AIRBOSS:GetRadialCase3(magnetic, offset) - -- Get radial. + -- Radial wrt angled runway. local radial=self:GetFinalBearing(magnetic)-180 -- Holding offset angle (+-15 or 30 degrees usually) @@ -3780,8 +4055,7 @@ function AIRBOSS:_GetRelativeHeading(unit, runway) local vP=unit:GetOrientationX() -- We only want the X-Z plane. Aircraft could fly parallel but ballistic and we dont want the "pitch" angle. - vC.y=0 - vP.y=0 + vC.y=0 ; vP.y=0 -- Get angle between the two orientation vectors in rad. local rhdg=math.deg(math.acos(UTILS.VecDot(vC,vP)/UTILS.VecNorm(vC)/UTILS.VecNorm(vP))) @@ -5614,7 +5888,7 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then -- Heading and distance to platform zone. - local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self.zonePlatform:GetCoordinate()) + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self:_GetZonePlatform(playerData.case):GetCoordinate()) local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate())) local fb=self:GetFinalBearing(true) @@ -5685,45 +5959,49 @@ function AIRBOSS:_MarkCase23Zones(_unitName, flare) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then + + local case=playerData.case + + if case<2 then + case=3 + end + -- Initial - local text="Marking CASE II/III zone:\n" + local text=string.format("Marking CASE %d zone:\n", case) --TODO: Add height! if flare then - text=text.."* valid area with GREEN flares\n" - local zp=self:_GetCase23ValidZone() - zp:FlareZone(FLARECOLOR.Green, 45) + text=text.."* approach corridor with GREEN flares\n" + self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) text=text.."* platform with RED flares\n" - self.zonePlatform:FlareZone(FLARECOLOR.Red, 45) + self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red, 45) text=text.."* dirty up with YELLOW flares\n" - self.zoneDirtyup:FlareZone(FLARECOLOR.Yellow, 45) + self:_GetZoneDirtyUp(case):FlareZone(FLARECOLOR.Yellow, 45) if math.abs(self.holdingoffset)>0 then - self.zoneArcturn1:FlareZone(FLARECOLOR.Yellow, 45) + self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.Yellow, 45) text=text.."* arc turn in with YELLOW flares\n" - self.zoneArcturn2:FlareZone(FLARECOLOR.White, 45) + self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White, 45) text=text.."* arc trun out with WHITE flares\n" end text=text.."* bullseye with WHITE flares\n" - self.zoneBullseye:FlareZone(FLARECOLOR.White, 45) + self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.White, 45) else - text=text.."* valid area with GREEN smoke\n" - local zp=self:_GetCase23ValidZone() - zp:SmokeZone(SMOKECOLOR.Green, 45) + text=text.."* approach corridor with GREEN smoke\n" + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) text=text.."* platform with RED smoke\n" - self.zonePlatform:SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) text=text.."* dirty up with ORANGE flares\n" + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) if math.abs(self.holdingoffset)>0 then - self.zoneArcturn1:SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Red, 45) text=text.."* arc turn in with YELLOW flares\n" - self.zoneArcturn2:SmokeZone(SMOKECOLOR.Orange, 45) + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Orange, 45) text=text.."* arc trun out with WHITE flares\n" end - - self.zoneDirtyup:SmokeZone(SMOKECOLOR.Orange, 45) text=text.."* bullseye with BLUE smoke\n" - self.zoneBullseye:SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Blue, 45) end -- Send message to player. From 0cf5bee09a64b9ec7fabf2fba6f4f81fdbce187a Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 28 Nov 2018 11:52:02 +0100 Subject: [PATCH 064/147] Pics --- Moose Development/Moose/Ops/Airboss.lua | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index f947fb967..635db4268 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -96,7 +96,17 @@ -- -- ## CASE I -- --- When CASE I recovery is active, +-- ### Holding Pattern +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Holding.png) +-- +-- ### Landing Pattern +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Landing.png) +-- +-- ## CASE III +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_CaseIII.png) -- -- @field #AIRBOSS AIRBOSS = { From 5e8b461478727a194a785db242989e886c883a98 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 28 Nov 2018 17:17:05 +0100 Subject: [PATCH 065/147] AIRBOSS v0.3.6w --- Moose Development/Moose/Ops/Airboss.lua | 46 ++++++++++++++++--------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 64aad99e9..e1aecbaeb 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -102,7 +102,11 @@ -- -- ## CASE III -- --- ![Banner Image](..\Presentations\AIRBOSS\Airboss_CaseIII.png) +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3.png) +-- +-- ## CASE II +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case2.png) -- -- @field #AIRBOSS AIRBOSS = { @@ -2832,8 +2836,10 @@ function AIRBOSS:_GetZoneArcIn(case) end + local x=12/math.cos(math.rad(self.holdingoffset)) + -- Distance = 14 NM - local distance=UTILS.NMToMeters(12/math.cos(math.rad(self.holdingoffset))) + local distance=UTILS.NMToMeters(x) -- Get coordinate and vec2. local coord=self:GetCoordinate():Translate(distance, radial) @@ -2914,22 +2920,26 @@ function AIRBOSS:_GetZoneCorridor(case) local w=UTILS.NMToMeters(5) -- Length of the box. local l=UTILS.NMToMeters(10) + + -- Angle between radial and offset in rad. + local alpha=math.rad(self.holdingoffset) + + -- Distance from ArcIn to ArcOut zone + local d=UTILS.NMToMeters(12) + local y=d*math.tan(alpha) - local beta=radial-self.holdingoffset - --env.info("FF beta = "..beta) - local x=(50-9)/math.cos(math.rad(beta)) - --env.info("FF x [NM] = "..x) + local d2=math.cos(alpha)/UTILS.NMToMeters(2) local c={} - c[1]=self:GetCoordinate() - c[2]=c[1]:Translate( UTILS.NMToMeters( 1), radial-90) - c[3]=c[2]:Translate( UTILS.NMToMeters(13), radial) - c[4]=c[3]:Translate( UTILS.NMToMeters( 2), radial+90) - c[5]=c[4]:Translate( UTILS.NMToMeters(10), offset) - c[6]=c[5]:Translate( UTILS.NMToMeters( 2), radial+90) - c[7]=c[6]:Translate(-UTILS.NMToMeters(10), offset) --This is more difficult! - c[8]=c[7]:Translate( UTILS.NMToMeters( 2), radial-90) - c[9]=c[8]:Translate(-UTILS.NMToMeters(13), radial) + c[1]=self:GetCoordinate() -- Carrier coordinate + c[2]=c[1]:Translate( UTILS.NMToMeters( 1), radial-90) -- 1 Right of carrier + c[3]=c[2]:Translate( UTILS.NMToMeters(13), radial) -- 1 Right and 13 "south" + c[4]=c[3]:Translate( UTILS.NMToMeters( y), radial+90) -- y left, 13 south + c[5]=c[4]:Translate( UTILS.NMToMeters(10), offset) -- to back wall angled + c[6]=c[5]:Translate( UTILS.NMToMeters( 2), offset+90) -- Back wall (angled) + c[7]=c[6]:Translate(-UTILS.NMToMeters(10+d2), offset) -- back along X & Z + c[8]=c[7]:Translate( UTILS.NMToMeters( y), radial-90) -- back along X + c[9]=c[1]:Translate(-UTILS.NMToMeters( 1), radial+90) -- 1 left of carrier -- Create an array of a square! @@ -3114,7 +3124,11 @@ function AIRBOSS:_Platform(playerData) end -- Next step: Dirty up and level out at 1200 ft. - playerData.step=AIRBOSS.PatternStep.DIRTYUP + if math.abs(self.holdingoffset)>0 then + playerData.step=AIRBOSS.PatternStep.ARCIN + else + playerData.step=AIRBOSS.PatternStep.DIRTYUP + end playerData.warning=nil end end From 50c40cb4e9d9851521014d3b52ae0d0283fd59c7 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 28 Nov 2018 23:55:14 +0100 Subject: [PATCH 066/147] AIRBOSS v0.3.9 --- Moose Development/Moose/Ops/Airboss.lua | 367 +++++++++++++++--------- 1 file changed, 226 insertions(+), 141 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index e1aecbaeb..0bcecf6d3 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -7,7 +7,7 @@ -- * CASE I, II and III recoveries. -- * Supports human pilots as well as AI flight groups. -- * Automatic LSO grading. --- * Different skill levels from tipps on-the-fly for students to complete ziplip for pros. +-- * Different skill levels from on-the-fly tipps for students to ziplip for pros. -- * Recovery tanker option. -- * Voice overs for LSO and AIRBOSS calls. Can easily be customized by users. -- * Automatic TACAN and ICLS channel setting. @@ -58,7 +58,6 @@ -- @field #AIRBOSS.Checkpoint Wake Right behind the carrier. -- @field #AIRBOSS.Checkpoint Groove In the groove checkpoint. -- @field #AIRBOSS.Checkpoint Trap Landing checkpoint. --- @field #AIRBOSS.Checkpoint Descent4k Case II/III descent at 4000 ft/min right after leaving holding pattern. -- @field #AIRBOSS.Checkpoint Platform Case II/III descent at 2000 ft/min at 5000 ft platform. -- @field #AIRBOSS.Checkpoint DirtyUp Case II/III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. -- @field #AIRBOSS.Checkpoint Bullseye Case III intercept glideslope and follow ICLS aka "bullseye". @@ -140,7 +139,6 @@ AIRBOSS = { Wake = {}, Groove = {}, Trap = {}, - Descent4k = {}, Platform = {}, DirtyUp = {}, Bullseye = {}, @@ -220,10 +218,13 @@ AIRBOSS.CarrierType={ -- @type AIRBOSS.PatternStep AIRBOSS.PatternStep={ UNDEFINED="Undefined", + REFUELING="Refueling", + SPINNING="Spinning", COMMENCING="Commencing", HOLDING="Holding", - DESCENT4K="Descent 4000 ft/min", PLATFORM="Platform", + ARCIN="Arc Turn In", + ARCOUT="Arc Turn Out", DIRTYUP="Level out and Dirty Up", BULLSEYE="Follow Bullseye", INITIAL="Initial", @@ -462,7 +463,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.7" +AIRBOSS.version="0.3.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -477,7 +478,7 @@ AIRBOSS.version="0.3.7" -- TODO: Generalize parameters for other aircraft. -- TODO: Foul deck check. -- TODO: Persistence of results. --- TODO: Strike group with helo bringing cargo etc. +-- NOPE: Strike group with helo bringing cargo etc. -- TODO: Right pattern step after bolter/wo/patternWO? -- TODO: CASE II. -- TODO: CASE III. @@ -556,20 +557,19 @@ function AIRBOSS:New(carriername, alias) self:SetTACAN() -- Set max aircraft in landing pattern. - self:SetMaxLandingPattern(1) + self:SetMaxLandingPattern(1) -- Set holding offset to 0 degrees. self:SetHoldingOffsetAngle(30) -- Default recovery case. - self:SetRecoveryCase(1) + self:SetRecoveryCase(3) -- CCA 50 NM radius zone around the carrier. self:SetCarrierControlledArea() -- CCZ 5 NM radius zone around the carrier. - self:SetCarrierControlledZone() - + self:SetCarrierControlledZone() -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then @@ -593,20 +593,14 @@ function AIRBOSS:New(carriername, alias) -- Smoke zones. - if self.Debug then - --self.zoneInitial:SmokeZone(SMOKECOLOR.White, 90) - --self.zonePlatform:SmokeZone(SMOKECOLOR.Orange, 90) - --self.zoneDirtyup:SmokeZone(SMOKECOLOR.Blue, 90) - --self.zoneBullseye:SmokeZone(SMOKECOLOR.Red, 90) - --self.zoneInitial:SmokeZone(SMOKECOLOR.White, 90) - local case=3 + if self.Debug and false then + local case=2 self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) - self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) - + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) end -- Init default sound files. @@ -837,7 +831,6 @@ function AIRBOSS:SetCarrierradio(frequency, modulation) return self end - --- Set number of aircraft units which can be in the landing pattern before the pattern is full. -- @param #AIRBOSS self -- @param #number nmax Max number. Default 4. @@ -856,7 +849,6 @@ function AIRBOSS:SetRecoveryTanker(recoverytanker) return self end - --- Define warehouse associated with the carrier. -- @param #AIRBOSS self -- @param Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. @@ -866,7 +858,6 @@ function AIRBOSS:SetWarehouse(warehouse) return self end - --- Check if carrier is recovering aircraft. -- @param #AIRBOSS self -- @return #boolean If true, time slot for recovery is open. @@ -1066,9 +1057,6 @@ end function AIRBOSS:onbeforeCase(From, Event, To, OldCase, NewCase) self.case=NewCase - - - end @@ -1121,17 +1109,6 @@ function AIRBOSS:_InitStennis() q2:BigSmokeSmall(0.1)--:SmokeBlue() ]] - -- 4k descent from holding pattern to 5k platform. - self.Descent4k.name="Descent 4k" - self.Descent4k.Xmin=-UTILS.NMToMeters(50) -- Not more than 50 NM behind the boat. - self.Descent4k.Xmax=nil -- -UTILS.NMToMeters(20) -- Not more than 20 NM closer to the boat from behind. - self.Descent4k.Zmin=-UTILS.NMToMeters(15) -- Not more than 15 NM port/left of boat. - self.Descent4k.Zmax= UTILS.NMToMeters(5) -- Not more than 5 NM starboard/right of boat. - self.Descent4k.LimitXmin=nil - self.Descent4k.LimitXmax=-UTILS.NMToMeters(21) -- Check and next step when 21 NM behind the boat. - self.Descent4k.LimitZmin=nil - self.Descent4k.LimitZmax=nil - -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight. self.Platform.name="Platform 5k" self.Platform.Xmin=-UTILS.NMToMeters(22) -- Not more than 22 NM behind the boat. Last check was at 21 NM. @@ -1281,19 +1258,23 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) local dist local speed - if step==AIRBOSS.PatternStep.DESCENT4K then - - speed=UTILS.KnotsToMps(250) - - elseif step==AIRBOSS.PatternStep.PLATFORM then + if step==AIRBOSS.PatternStep.PLATFORM then alt=UTILS.FeetToMeters(5000) dist=UTILS.NMToMeters(20) speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.ARCIN then + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.ARCOUT then + + speed=UTILS.KnotsToMps(250) - elseif step==AIRBOSS.PatternStep.DIRTYUP then + elseif step==AIRBOSS.PatternStep.DIRTYUP then alt=UTILS.FeetToMeters(1200) @@ -2079,6 +2060,11 @@ function AIRBOSS:_InitPlayer(playerData) playerData.landed=false playerData.Tlso=timer.getTime() + if playerData.group:GetName():match("Groove") then + self:MessageToPlayer(playerData, "Group name contains Groove. You are supposed to test the groove.") + playerData.step=AIRBOSS.PatternStep.FINAL + end + return playerData end @@ -2275,13 +2261,10 @@ function AIRBOSS:_CheckPlayerStatus() local time=timer.getAbsTime() local clock=UTILS.SecondsToClock(time) self:T3(string.format("Player status undefined. Waiting for next step. Time %s", clock)) - - - -- Jump to final/groove for testing. - if self.groovedebug then - playerData.step=AIRBOSS.PatternStep.FINAL - self.groovedebug=false - end + + elseif playerData.step==AIRBOSS.PatternStep.REFUELING then + + -- Nothing to do here at the moment. elseif playerData.step==AIRBOSS.PatternStep.HOLDING then @@ -2293,19 +2276,24 @@ function AIRBOSS:_CheckPlayerStatus() -- CASE I/II/III: New approach. self:_Commencing(playerData) - elseif playerData.step==AIRBOSS.PatternStep.DESCENT4K then - - -- CASE II/III: Initial descent with 4000 ft/min. - self:_Descent4k(playerData) - elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then -- CASE II/III: Player has reached 5k "Platform". self:_Platform(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.ARCIN then + + -- Case II/III if offset. + self:_ArcInTurn(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.ARCOUT then + + -- Case II/III if offset. + self:_ArcOutTurn(playerData) elseif playerData.step==AIRBOSS.PatternStep.DIRTYUP then - -- CASE II/III: Player has descended to 1200 ft and is going level from now on. + -- CASE III: Player has descended to 1200 ft and is going level from now on. self:_DirtyUp(playerData) elseif playerData.step==AIRBOSS.PatternStep.BULLSEYE then @@ -2374,6 +2362,10 @@ function AIRBOSS:_CheckPlayerStatus() -- Undefined status. playerData.step=AIRBOSS.PatternStep.UNDEFINED + else + + self:E(self.lid..string.format("ERROR: unknown player step %s", tostring(playerData.step))) + end else @@ -2438,12 +2430,9 @@ function AIRBOSS:OnEventBirth(EventData) --self:RadioTransmission(self.LSOradio, self.radiocall.LONGINGROOVE, false, 20) if self.Debug then - self:_MarkCase23Zones(_unit:GetName()) + --self:_MarkCase23Zones(_unit:GetName()) end - -- Start in the groove for debugging. - self.groovedebug=false - end end @@ -2500,6 +2489,10 @@ function AIRBOSS:OnEventLand(EventData) -- Landing distance to carrier position. local dist=coord:Get2DDistance(self:GetCoordinate()) + if X<0 then + dist=-dist + end + -- TODO: check if 360 degrees correctino is necessary! local hdg=self.carrier:GetHeading()+self.carrierparam.rwyangle @@ -2527,7 +2520,7 @@ function AIRBOSS:OnEventLand(EventData) playerData.step=AIRBOSS.PatternStep.UNDEFINED -- Call trapped function in 3 seconds to make sure we did not bolter. - SCHEDULER:New(nil, self._Trapped,{self, playerData, dist}, 3) + SCHEDULER:New(nil, self._Trapped,{self, playerData, wire}, 3) end else @@ -2925,10 +2918,10 @@ function AIRBOSS:_GetZoneCorridor(case) local alpha=math.rad(self.holdingoffset) -- Distance from ArcIn to ArcOut zone - local d=UTILS.NMToMeters(12) + local d=12 local y=d*math.tan(alpha) - local d2=math.cos(alpha)/UTILS.NMToMeters(2) + local d2=math.cos(alpha)/2 local c={} c[1]=self:GetCoordinate() -- Carrier coordinate @@ -2938,8 +2931,8 @@ function AIRBOSS:_GetZoneCorridor(case) c[5]=c[4]:Translate( UTILS.NMToMeters(10), offset) -- to back wall angled c[6]=c[5]:Translate( UTILS.NMToMeters( 2), offset+90) -- Back wall (angled) c[7]=c[6]:Translate(-UTILS.NMToMeters(10+d2), offset) -- back along X & Z - c[8]=c[7]:Translate( UTILS.NMToMeters( y), radial-90) -- back along X - c[9]=c[1]:Translate(-UTILS.NMToMeters( 1), radial+90) -- 1 left of carrier + c[8]=c[7]:Translate( UTILS.NMToMeters( y), radial-90) -- back along X + c[9]=c[1]:Translate( UTILS.NMToMeters( 1), radial+90) -- 1 left of carrier -- Create an array of a square! @@ -2991,13 +2984,16 @@ function AIRBOSS:_GetHoldingZone(playerData) else -- CASE II/II + + -- Get radial. + local hdg + if playerData.case==2 then + hdg=self:GetRadialCase2(false, true) + else + hdg=self:GetRadialCase3(false, true) + end - -- TODO: Include 15 or 30 degrees offset. - local hdg=self.carrier:GetHeading()-self.holdingoffset - - --TODO: case dependend radial! - local hdg=self:GetRadialCase3(false) - + -- TODO: This is WRONG! -- Create an array of a square! local p={} p[1]=c1:Translate(UTILS.NMToMeters(1), hdg+90):GetVec2() --c1 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. @@ -3033,12 +3029,12 @@ function AIRBOSS:_Commencing(playerData) -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. playerData.step=AIRBOSS.PatternStep.INITIAL else - -- CASE III: Player has to start the descent at 4000 ft/min. + -- CASE II/III: Player has to start the descent at 4000 ft/min. playerData.step=AIRBOSS.PatternStep.PLATFORM end end ---- Start pattern when player enters the initial zone. +--- Start pattern when player enters the initial zone in case I/II recoveries. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Initial(playerData) @@ -3061,28 +3057,7 @@ function AIRBOSS:_Initial(playerData) end ---- Descent at 4k. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Descent4k(playerData) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi=self:_GetDistances(playerData.unit) - - -- Abort condition check. - if self:_CheckAbort(X, Z, self.Descent4k) then - self:_AbortPattern(playerData, X, Z, self.Descent4k) - --return - end - - -- Check if we are in front of the boat (diffX > 0). - if self:_CheckLimits(X, Z, self.Descent4k) then - -- Next step: Platform at 5k - playerData.step=AIRBOSS.PatternStep.PLATFORM - end -end - ---- Platform at 5k ft. Descent at 2000 ft/min. +--- Platform at 5k ft for case II/III recoveries. Descent at 2000 ft/min. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Platform(playerData) @@ -3095,7 +3070,7 @@ function AIRBOSS:_Platform(playerData) -- Issue warning. if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid pattern zone!", "AIRBOSS") + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") playerData.warning=true end @@ -3123,17 +3098,123 @@ function AIRBOSS:_Platform(playerData) self:MessageToPlayer(playerData, hint, "MARSHAL", "") end - -- Next step: Dirty up and level out at 1200 ft. + -- Next step: depends. if math.abs(self.holdingoffset)>0 then + -- Turn to BRC (case I) or FB (case III). playerData.step=AIRBOSS.PatternStep.ARCIN else - playerData.step=AIRBOSS.PatternStep.DIRTYUP + if playerData.case==2 then + -- Case II: Initial zone then Case I recovery. + playerData.step=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- CASE III: Dirty up. + playerData.step=AIRBOSS.PatternStep.DIRTYUP + end end playerData.warning=nil end end ---- Dirty up and level out at 1200 ft. + +--- Arc in turn for case II/III recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_ArcInTurn(playerData) + + -- Check if player is in valid zone + local validzone=self:_GetZoneCorridor(playerData.case) + + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and not playerData.warning then + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") + playerData.warning=true + end + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneArcIn(playerData.case)) + + --if self:_CheckLimits(X, Z, self.DirtyUp) then + if inzone then + + -- Debug message. + MESSAGE:New("Arc Turn In step reached", 5):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get speed hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s", playerData.step, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: Arc Out Turn. + playerData.step=AIRBOSS.PatternStep.ARCOUT + + playerData.warning=nil + end +end + +--- Arc out turn for case II/III recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_ArcOutTurn(playerData) + + -- Check if player is in valid zone + local validzone=self:_GetZoneCorridor(playerData.case) + + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and not playerData.warning then + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") + playerData.warning=true + end + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneArcOut(playerData.case)) + + --if self:_CheckLimits(X, Z, self.DirtyUp) then + if inzone then + + -- Debug message. + MESSAGE:New("Arc Turn Out step reached", 5):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get speed hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s", playerData.step, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: + if playerData.case==2 then + -- Case II: Initial. + playerData.step=AIRBOSS.PatternStep.INITIAL + elseif playerdata.case==3 then + -- Case III: Dirty up. + playerData.step=AIRBOSS.PatternStep.DIRTYUP + else + -- ERROR! + end + + playerData.warning=nil + end +end + +--- Dirty up and level out at 1200 ft for case III recovery. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_DirtyUp(playerData) @@ -3146,8 +3227,8 @@ function AIRBOSS:_DirtyUp(playerData) -- Issue warning. if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid pattern zone!", "AIRBOSS") - playerData.warning=true + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") + playerData.warning=true end -- Check if we are inside the moving zone. @@ -3175,19 +3256,14 @@ function AIRBOSS:_DirtyUp(playerData) self:MessageToPlayer(playerData, hint, "MARSHAL", "") end - -- Next step: - if self.case==2 then - -- CASE II: Fly to the initial and perform CASE I pattern. - playerData.step=AIRBOSS.PatternStep.INITIAL - elseif self.case==3 then - -- CASE III: Intercept glide slope and follow bullseye (ICLS). - playerData.step=AIRBOSS.PatternStep.BULLSEYE - end + -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). + playerData.step=AIRBOSS.PatternStep.BULLSEYE + playerData.warning=nil end end ---- Intercept glide slop and follow ICLS, aka Bullseye. +--- Intercept glide slop and follow ICLS, aka Bullseye for case III recovery. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Bullseye(playerData) @@ -3200,7 +3276,7 @@ function AIRBOSS:_Bullseye(playerData) -- Issue warning. if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid pattern zone!", "AIRBOSS") + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") playerData.warning=true end @@ -3231,12 +3307,13 @@ function AIRBOSS:_Bullseye(playerData) -- Next step: Groove Call the ball. playerData.step=AIRBOSS.PatternStep.GROOVE_XX + playerData.warning=nil end end ---- Upwind leg or break entry. +--- Upwind leg or break entry for case I/II recoveries. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Upwind(playerData) @@ -3781,13 +3858,13 @@ function AIRBOSS:_GetWire(d) -- Which wire was caught? X>0 since calculated as distance! local wire - if d>math.abs(self.carrierparam.wire1+wdx) then + if dmath.abs(self.carrierparam.wire2+wdx) then + elseif dmath.abs(self.carrierparam.wire3+wdx) then + elseif dmath.abs(self.carrierparam.wire4+wdx) then + elseif d Boltered! playerData.boltered=true end @@ -4012,7 +4076,7 @@ function AIRBOSS:GetRadialCase2(magnetic, offset) -- Holding offset angle (+-15 or 30 degrees usually) if offset then - radial=radial-self.holdingoffset + radial=radial+self.holdingoffset end -- Adjust for negative values. @@ -4035,7 +4099,7 @@ function AIRBOSS:GetRadialCase3(magnetic, offset) -- Holding offset angle (+-15 or 30 degrees usually) if offset then - radial=radial-self.holdingoffset + radial=radial+self.holdingoffset end -- Adjust for negative values. @@ -5285,14 +5349,20 @@ function AIRBOSS:_ResetPlayerStatus(_unitName) if playerData then + -- Inform player. local text="Status reset executed! You have been removed from all queues." - + self:MessageToPlayer(playerData, text, nil, "") + + -- Remove from marhal stack can collapse stack if necessary. if self:_InQueue(self.Qmarshal, playerData.group) then self:_CollapseMarshalStack(playerData, true) - end + end -- Remove flight from queues. self:_RemoveFlight(playerData) + + -- Initialize player data. + self:_InitPlayer(playerData) end end @@ -5770,6 +5840,21 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) icls=string.format("%d", self.ICLSchannel) end + -- Get groups, units in queues. + local Nmarshal,nmarshal=self:_GetQueueInfo(self.Qmarshal, playerData.case) + local Npattern,npattern=self:_GetQueueInfo(self.Qpattern) + + -- Get recovery times of carrier. + local recoverytimes="Recovery time slots:" + if #self.recoverytime==0 then + recoverytimes=recoverytimes.." empty" + else + for _,_rtime in pairs(self.recoverytime) do + local rtime=_rtime --#AIRBOSS.Recovery + recoverytimes=recoverytimes..string.format("\nSlot %s - %s", UTILS.SecondsToClock(rtime.START), UTILS.SecondsToClock(rtime.STOP)) + end + end + -- Message text. local text=string.format("%s info:\n", self.alias) text=text..string.format("=============================================\n") @@ -5783,8 +5868,9 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) text=text..string.format("TACAN Channel %s\n", tacan) text=text..string.format("ICLS Channel %s\n", icls) text=text..string.format("# A/C total %d\n", #self.flights) - text=text..string.format("# A/C holding %d\n", #self.Qmarshal) - text=text..string.format("# A/C pattern %d", #self.Qpattern) + text=text..string.format("# A/C marshal %d (%d)\n", Nmarshal, nmarshal) + text=text..string.format("# A/C pattern %d (%d)\n", Npattern, npattern) + text=text..string.format(recoverytimes) self:T2(self.lid..text) -- Send message. @@ -5989,8 +6075,7 @@ function AIRBOSS:_MarkCase23Zones(_unitName, flare) if case<2 then case=3 end - - + -- Initial local text=string.format("Marking CASE %d zone:\n", case) From 99c2e76e539bda392e9d7a0e5659418f72ce2180 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 29 Nov 2018 16:19:19 +0100 Subject: [PATCH 067/147] AIRBOSS v0.3.9w --- Moose Development/Moose/Core/Radio.lua | 2 +- Moose Development/Moose/Ops/Airboss.lua | 413 ++++++++++++++++-------- 2 files changed, 279 insertions(+), 136 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index ac45e3cc0..f01cb9a09 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -348,7 +348,7 @@ end -- * If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored. -- * If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration are ignored -- @param #RADIO self --- @param #boolean trigger Use trigger.action.radioTransmission() in any case, i.e. also for UNITS and GROUPS. +-- @param #boolean viatrigger Use trigger.action.radioTransmission() in any case, i.e. also for UNITS and GROUPS. -- @return #RADIO self function RADIO:Broadcast(viatrigger) self:F({viatrigger=viatrigger}) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 0bcecf6d3..dd029789b 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -45,6 +45,7 @@ -- @field Core.Radio#RADIO LSOradio Radio for LSO calls. -- @field Core.Radio#RADIO Carrierradio Radio for carrier calls. -- @field #AIRBOSS.RadioCalls radiocall LSO and Airboss call sound files and texts. +-- @field Core.Scheduler#SCHEDULER radiotimer Radio queue scheduler. -- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneInitial Zone usually 3 NM astern of carrier where pilots start their CASE I pattern. @@ -65,6 +66,8 @@ -- @field #table flights List of all flights in the CCA. -- @field #table Qmarshal Queue of marshalling aircraft groups. -- @field #table Qpattern Queue of aircraft groups in the landing pattern. +-- @field #table RQMarshal Radio queue of marshal. +-- @field #table RQLSO Radio queue of LSO. -- @field #number Nmaxpattern Max number of aircraft in landing pattern. -- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. -- @field Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. @@ -126,6 +129,7 @@ AIRBOSS = { Carrierradio = nil, Carrierfreq = nil, radiocall = {}, + radiotimer = nil, zoneCCA = nil, zoneCCZ = nil, zoneInitial = nil, @@ -146,6 +150,8 @@ AIRBOSS = { flights = {}, Qpattern = {}, Qmarshal = {}, + RQMarshal = {}, + RQLSO = {}, Nmaxpattern = nil, tanker = nil, warehouse = nil, @@ -463,7 +469,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.9" +AIRBOSS.version="0.3.9w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -550,6 +556,9 @@ function AIRBOSS:New(carriername, alias) self.LSOradio:SetAlias("LSO") self:SetLSOradio() + -- Radio scheduler. + self.radiotimer=SCHEDULER:New() + -- Set ICSL to channel 1. self:SetICLS() @@ -908,6 +917,11 @@ function AIRBOSS:onafterStart(From, Event, To) -- Time stamp for checking queues. self.Tqueue=timer.getTime() + + -- Schedule radio queue checks. + -- TODO: id's to self to be able to stop the scheduler. + local RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self, self.RQLSO}, 1, 0.1) + local RQMid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self, self.RQMarshal}, 1, 0.1) -- Start status check in 1 second. self:__Status(1) @@ -1728,8 +1742,7 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) -- Carrier position. local Carrier=self:GetCoordinate() - local hdg=self.carrier:GetHeading() - + -- Altitude of first stack. Depends on recovery case. local angels0 local Dist @@ -1739,14 +1752,36 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) if case==1 then -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next stack. angels0=2 + + -- Distance 2.5 NM. Dist=UTILS.NMToMeters(2.5) + + -- Get true heading of carrier. + local hdg=self.carrier:GetHeading() + + -- Center of holding pattern point. We give it a little head start -70 instead of -90 degrees. p1=Carrier:Translate(Dist, hdg-70) else -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. angels0=6 + + -- Distance: d=n*angles0+15 NM, so first stack is at 15+6=21 NM Dist=UTILS.NMToMeters((stack-1)*angels0+15) - p1=Carrier:Translate(-Dist, hdg) - p2=Carrier:Translate(-(Dist+UTILS.NMToMeters(10)), hdg) + + -- Get correct radial depending on recovery case including offset. + local radial + if case==2 then + radial=self:GetRadialCase2(false, true) + elseif case==3 then + radial=self:GetRadialCase3(false, true) + end + + -- First point of race track pattern + p1=Carrier:Translate(Dist, radial) + + -- Second point which is 10 NM further behind. + --TODO: check if 10 NM is okay. + p2=Carrier:Translate(Dist+UTILS.NMToMeters(10), radial) end -- Pattern altitude. @@ -2968,15 +3003,18 @@ function AIRBOSS:_GetHoldingZone(playerData) -- Current stack. local stack=playerData.flag:Get() + -- Player's recovery case. + local case=playerData.case + -- Stack is <= 0 ==> no marshal zone. if stack<=0 then return nil - end + end -- Pattern alitude. - local patternalt, c1, c2=self:_GetMarshalAltitude(stack) + local patternalt, c1, c2=self:_GetMarshalAltitude(stack, case) - if playerData.case==1 then + if case==1 then -- CASE I -- Zone 2.5 NM port of carrier with a radius of 3 NM (holding pattern should be < 5 NM). @@ -2987,13 +3025,12 @@ function AIRBOSS:_GetHoldingZone(playerData) -- Get radial. local hdg - if playerData.case==2 then + if case==2 then hdg=self:GetRadialCase2(false, true) else hdg=self:GetRadialCase3(false, true) end - -- TODO: This is WRONG! -- Create an array of a square! local p={} p[1]=c1:Translate(UTILS.NMToMeters(1), hdg+90):GetVec2() --c1 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. @@ -3136,7 +3173,6 @@ function AIRBOSS:_ArcInTurn(playerData) -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZoneArcIn(playerData.case)) - --if self:_CheckLimits(X, Z, self.DirtyUp) then if inzone then -- Debug message. @@ -4253,19 +4289,19 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) if glideslopeError>1 then -- "You're high!" self:RadioTransmission(self.LSOradio, self.radiocall.HIGH, true, delay) - delay=delay+1.5 + --delay=delay+1.5 elseif glideslopeError>0.5 then -- "You're a little high." self:RadioTransmission(self.LSOradio, self.radiocall.HIGH, false, delay) - delay=delay+1.5 + --delay=delay+1.5 elseif glideslopeError<-1.0 then -- "Power!" self:RadioTransmission(self.LSOradio, self.radiocall.POWER, true, delay) - delay=delay+1.5 + --delay=delay+1.5 elseif glideslopeError<-0.5 then -- "You're a little low." self:RadioTransmission(self.LSOradio, self.radiocall.POWER, false, delay) - delay=delay+1.5 + --delay=delay+1.5 else text="Good altitude." end @@ -4277,19 +4313,19 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) if lineupError<-3 then -- "Come left!" self:RadioTransmission(self.LSOradio, self.radiocall.COMELEFT, true, delay) - delay=delay+1.5 + --delay=delay+1.5 elseif lineupError<-1 then -- "Come left." self:RadioTransmission(self.LSOradio, self.radiocall.COMELEFT, false, delay) - delay=delay+1.5 + --delay=delay+1.5 elseif lineupError>3 then -- "Right for lineup!" self:RadioTransmission(self.LSOradio, self.radiocall.RIGHTFORLINEUP, true, delay) - delay=delay+1.5 + --delay=delay+1.5 elseif lineupError>1 then -- "Right for lineup." self:RadioTransmission(self.LSOradio, self.radiocall.RIGHTFORLINEUP, false, delay) - delay=delay+1.5 + --delay=delay+1.5 else text=text.."Good lineup." end @@ -4303,21 +4339,21 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) if aoa>=9.3 then -- "Your're slow!" self:RadioTransmission(self.LSOradio, self.radiocall.SLOW, true, delay) - delay=delay+1.5 + --delay=delay+1.5 elseif aoa>=8.8 and aoa<9.3 then -- "Your're a little slow." self:RadioTransmission(self.LSOradio, self.radiocall.SLOW, false, delay) - delay=delay+1.5 + --delay=delay+1.5 elseif aoa>=7.4 and aoa<8.8 then text=text.."You're on speed." elseif aoa>=6.9 and aoa<7.4 then -- "You're a little fast." self:RadioTransmission(self.LSOradio, self.radiocall.FAST, false, delay) - delay=delay+1.5 + --delay=delay+1.5 elseif aoa>=0 and aoa<6.9 then -- "You're fast!" self:RadioTransmission(self.LSOradio, self.radiocall.FAST, true, delay) - delay=delay+1.5 + --delay=delay+1.5 else text=text.."Unknown AoA state." end @@ -4969,117 +5005,6 @@ end -- MISC functions ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Radio transmission. --- @param #AIRBOSS self --- @param Core.Radio#RADIO radio sending transmission. --- @param #AIRBOSS.RadioSound call Radio sound files and subtitles. --- @param #boolean loud If true, play loud sound file version. --- @param #number delay Delay in seconds, before the message is broadcasted. -function AIRBOSS:RadioTransmission(radio, call, loud, delay) - self:E({radio=radio, call=call, loud=loud, delay=delay}) - - if (delay==nil) or (delay and delay==0) then - - if call==nil then - self:E(self.lid.."ERROR: Radio call=nil!") - self:E({radio=radio}) - self:E({call=call}) - self:E({loud=loud}) - self:E({delay=delay}) - return - end - - local filename - if loud then - filename=call.louder - else - filename=call.normal - end - - -- New transmission. - radio:NewUnitTransmission(filename, call.subtitle, call.duration, radio.Frequency/1000000, radio.Modulation, false) - - -- Broadcast message. - radio:Broadcast(true) - - -- "Subtitle". - self:MessageToAll(call.subtitle, radio:GetAlias(), "", call.duration) - - else - - if call==nil then - self:E(self.lid.."ERROR: Radio call=nil!") - self:E({radio=radio}) - self:E({call=call}) - self:E({loud=loud}) - self:E({delay=delay}) - return - end - - -- Scheduled transmission. - SCHEDULER:New(nil, self.RadioTransmission, {self, radio, call, loud}, delay) - end -end - ---- Send text message to player client. --- Message format will be "SENDER: RECCEIVER, MESSAGE". --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data. --- @param #string message The message to send. --- @param #string sender The person who sends the message or nil. --- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. --- @param #number duration Display message duration. Default 10 seconds. --- @param #boolean clear If true, clear screen from previous messages. --- @param #number delay Delay in seconds, before the message is displayed. -function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay) - - if playerData and message and message~="" then - - -- Default duration. - duration=duration or 10 - - -- Format message. - local text - if receiver and receiver=="" then - text=string.format("%s", message) - else - receiver=receiver or playerData.onboard - text=string.format("%s, %s", receiver, message) - end - self:I(self.lid..text) - - if delay and delay>0 then - SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) - else - if playerData.client then - MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) - end - end - - end - -end - ---- Send text message to all players in the CCA. --- Message format will be "SENDER: RECCEIVER, MESSAGE". --- @param #AIRBOSS self --- @param #string message The message to send. --- @param #string sender The person who sends the message or nil. --- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. --- @param #number duration Display message duration. Default 10 seconds. --- @param #boolean clear If true, clear screen from previous messages. --- @param #number delay Delay in seconds, before the message is displayed. -function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay) - - for _,_player in pairs(self.players) do - local player=_player --#AIRBOSS.PlayerData - if player.unit:IsInZone(self.zoneCCA) then - self:MessageToPlayer(player,message,sender,receiver,duration,clear,delay) - end - end - -end - --- Check if aircraft is capable of landing on an aircraft carrier. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.) @@ -5226,7 +5151,7 @@ function AIRBOSS:_GetPlayerUnitAndName(_unitName) return nil,nil end ---- Get carrier coaltion. +--- Get carrier coalition. -- @param #AIRBOSS self -- @return #number Coalition side of carrier. function AIRBOSS:GetCoalition() @@ -5240,6 +5165,224 @@ function AIRBOSS:GetCoordinate() return self.carrier:GetCoordinate() end +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MESSAGE Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Radio queue item. +-- @type AIRBOSS.Radioitem +-- @field #number Tplay Abs time when transmission should be played. +-- @field #number duration Duration of the transmission in seconds. +-- @field #number Tstarted Abs time when transmission began to play. +-- @field #number prio Priority 0-100. +-- @field #boolean isplaying Currently playing. +-- @field Core.Beacon#RADIO radio Radio object. +-- @field #AIRBOSS.SoundFile call +-- @field #boolean loud Play loud version + +--- Check radio queue for transmissions to be broadcasted. +-- @param #AIRBOSS self +-- @param #table radioqueue The radio queue. +function AIRBOSS:_CheckRadioQueue(radioqueue) + + -- Check if queue is empty. + if #radioqueue==0 then + return + end + + -- Get current abs time. + local time=timer.getAbsTime() + + -- Sort results table wrt times they have already been engaged. + local function _sort(a, b) + return (a.Tplay < b.Tplay) or (a.Tplay==b.Tplay and a.prio < b.prio) + end + table.sort(radioqueue, _sort) + + local playing=false + local next=nil --#AIRBOSS.Radioitem + local remove=nil + for i,_transmission in ipairs(radioqueue) do + local transmission=_transmission --#AIRBOSS.Radioitem + + -- Check if transmission time has passed. + if time>transmission.Tplay then + + -- Check if transmission is currently playing. + if transmission.isplaying then + + -- Check if transmission is finished. + if time>transmission.Tstarted+transmission.duration then + + -- Transmission over. + transmission.isplaying=false + remove=i + --table.insert(remove, i) + + else -- still playing + + -- Transmission is still playing. + playing=true + + end + + else -- not playing yet + + -- Not playing ==> this will be next. + if next==nil then + next=transmission + end + + end + + else + + -- Transmission not due yet. + + end + end + + -- Found a new transmission. + if next~=nil and not playing then + self:RadioTransmit(next.radio, next.call, next.loud) + next.isplaying=true + next.Tstarted=time + end + + -- Remove completed calls from queue. + --for _,idx in pairs(remove) do + if remove then + table.remove(radioqueue, remove) + end + --end + +end + +--- Add Radio transmission to radio queue +-- @param #AIRBOSS self +-- @param Core.Radio#RADIO radio sending transmission. +-- @param #AIRBOSS.RadioSound call Radio sound files and subtitles. +-- @param #boolean loud If true, play loud sound file version. +-- @param #number delay Delay in seconds, before the message is broadcasted. +function AIRBOSS:RadioTransmission(radio, call, loud, delay) + self:E({radio=radio, call=call, loud=loud, delay=delay}) + + -- Create a new radio transmission item. + local transmission={} --#AIRBOSS.Radioitem + + transmission.radio=radio + transmission.call=call + transmission.loud=loud + transmission.Tplay=timer.getAbsTime()+delay + transmission.prio=50 + transmission.isplaying=false + transmission.Tstarted=nil + + -- Add transmission to the right queue. + if radio:GetAlias()=="LSO" then + + table.insert(self.RQLSO, transmission) + + elseif radio:GetAlias()=="AIRBOSS" then + + table.insert(self.RQMarshal, transmission) + + end +end + +--- Transmission radio message. +-- @param #AIRBOSS self +-- @param Core.Radio#RADIO radio sending transmission. +-- @param #AIRBOSS.RadioSound call Radio sound files and subtitles. +-- @param #boolean loud If true, play loud sound file version. +-- @param #number delay Delay in seconds, before the message is broadcasted. +function AIRBOSS:RadioTransmit(radio, call, loud, delay) + self:E({radio=radio, call=call, loud=loud, delay=delay}) + + if (delay==nil) or (delay and delay==0) then + + local filename + if loud then + filename=call.louder + else + filename=call.normal + end + + -- New transmission. + radio:NewUnitTransmission(filename, call.subtitle, call.duration, radio.Frequency/1000000, radio.Modulation, false) + + -- Broadcast message. + radio:Broadcast(true) + + -- "Subtitle". + self:MessageToAll(call.subtitle, radio:GetAlias(), "", call.duration) + + else + + -- Scheduled transmission. + SCHEDULER:New(nil, self.RadioTransmission, {self, radio, call, loud}, delay) + end +end + +--- Send text message to player client. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay) + + if playerData and message and message~="" then + + -- Default duration. + duration=duration or 10 + + -- Format message. + local text + if receiver and receiver=="" then + text=string.format("%s", message) + else + receiver=receiver or playerData.onboard + text=string.format("%s, %s", receiver, message) + end + self:I(self.lid..text) + + if delay and delay>0 then + SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) + else + if playerData.client then + MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) + end + end + + end + +end + +--- Send text message to all players in the CCA. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay) + + for _,_player in pairs(self.players) do + local player=_player --#AIRBOSS.PlayerData + if player.unit:IsInZone(self.zoneCCA) then + self:MessageToPlayer(player,message,sender,receiver,duration,clear,delay) + end + end + +end + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- RADIO MENU Functions ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- From 21f0094d7c97534f9a2341d97978dc02c79bf7c1 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 29 Nov 2018 23:41:57 +0100 Subject: [PATCH 068/147] AIRBOSS v0.4.0 --- .../Moose/Functional/Warehouse.lua | 18 ++-- Moose Development/Moose/Ops/Airboss.lua | 90 ++++++++++--------- Moose Development/Moose/Ops/RescueHelo.lua | 2 +- 3 files changed, 63 insertions(+), 47 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index f65037e6c..074c15f71 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1719,12 +1719,14 @@ WAREHOUSE.Quantity = { --- Warehouse database. Note that this is a global array to have easier exchange between warehouses. -- @type WAREHOUSE.db -- @field #number AssetID Unique ID of each asset. This is a running number, which is increased each time a new asset is added. --- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}. +-- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}.# +-- @field #number WarehouseID Unique ID of the warehouse. Running number. -- @field #table Warehouses Table holding all defined @{#WAREHOUSE} objects by their unique ids. WAREHOUSE.db = { - AssetID = 0, - Assets = {}, - Warehouses = {} + AssetID = 0, + Assets = {}, + WarehouseID = 0, + Warehouses = {} } --- Warehouse class version. @@ -1825,7 +1827,13 @@ function WAREHOUSE:New(warehouse, alias) -- Set some variables. self.warehouse=warehouse - self.uid=tonumber(warehouse:GetID()) + + -- Increase global warehouse counter. + WAREHOUSE.db.WarehouseID=WAREHOUSE.db.WarehouseID+1 + + -- Set unique ID for this warehouse. + self.uid=WAREHOUSE.db.WarehouseID + --self.uid=tonumber(warehouse:GetID()) -- Closest of the same coalition but within a certain range. local _airbase=self:GetCoordinate():GetClosestAirbase(nil, self:GetCoalition()) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index dd029789b..5159ceaf4 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -287,37 +287,37 @@ AIRBOSS.Soundfile={ normal="LSO - RightLineUp(S).ogg", louder="LSO - RightLineUp(L).ogg", subtitle="Right for line up.", - duration=3, + duration=1.5, }, COMELEFT={ normal="LSO - ComeLeft(S).ogg", louder="LSO - ComeLeft(L).ogg", subtitle="Come left.", - duration=3, + duration=1, }, HIGH={ normal="LSO - High(S).ogg", louder="LSO - High(L).ogg", subtitle="You're high.", - duration=3, + duration=1, }, POWER={ normal="LSO - Power(S).ogg", louder="LSO - Power(L).ogg", subtitle="Power.", - duration=3, + duration=1, }, SLOW={ normal="LSO-Slow-Normal.ogg", louder="LSO-Slow-Loud.ogg", subtitle="You're slow.", - duration=3, + duration=1, }, FAST={ normal="LSO-Fast-Normal.ogg", louder="LSO-Fast-Loud.ogg", subtitle="You're fast.", - duration=3, + duration=1, }, CALLTHEBALL={ normal="LSO - Call the Ball.ogg", @@ -328,17 +328,17 @@ AIRBOSS.Soundfile={ ROGERBALL={ normal="LSO - Roger.ogg", subtitle="Roger ball!", - duration=3, + duration=1.2, }, WAVEOFF={ normal="LSO - WaveOff.ogg", subtitle="Wave off!", - duration=3, + duration=1, }, BOLTER={ normal="LSO - Bolter.ogg", subtitle="Bolter, Bolter!", - duration=3, + duration=1.5, }, LONGINGROOVE={ normal="LSO - Long in Groove.ogg", @@ -469,25 +469,25 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.3.9w" +AIRBOSS.version="0.4.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Set case II and III times. --- TODO: Add radio transmission queue for LSO and airboss. -- TODO: Get correct wire when trapped. +-- TODO: Set case II and III times. -- TODO: Add radio check (LSO, AIRBOSS) to F10 radio menu. -- TODO: Add user functions. -- TODO: Generalize parameters for other carriers. -- TODO: Generalize parameters for other aircraft. -- TODO: Foul deck check. -- TODO: Persistence of results. --- NOPE: Strike group with helo bringing cargo etc. -- TODO: Right pattern step after bolter/wo/patternWO? --- TODO: CASE II. --- TODO: CASE III. +-- DONE: Add radio transmission queue for LSO and airboss. +-- TONE: CASE II. +-- DONE: CASE III. +-- NOPE: Strike group with helo bringing cargo etc. Not yet. -- DONE: Handle crash event. Delete A/C from queue, send rescue helo. -- DONE: Get fuel state in pounds. (working for the hornet, did not check others) -- DONE: Add aircraft numbers in queue to carrier info F10 radio output. @@ -920,8 +920,8 @@ function AIRBOSS:onafterStart(From, Event, To) -- Schedule radio queue checks. -- TODO: id's to self to be able to stop the scheduler. - local RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self, self.RQLSO}, 1, 0.1) - local RQMid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self, self.RQMarshal}, 1, 0.1) + local RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQLSO, "LSO"}, 1, 0.1) + local RQMid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQMarshal, "MARSHAL"}, 1, 0.1) -- Start status check in 1 second. self:__Status(1) @@ -1766,7 +1766,7 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) angels0=6 -- Distance: d=n*angles0+15 NM, so first stack is at 15+6=21 NM - Dist=UTILS.NMToMeters((stack-1)*angels0+15) + Dist=UTILS.NMToMeters(stack*angels0+15) -- Get correct radial depending on recovery case including offset. local radial @@ -2524,6 +2524,7 @@ function AIRBOSS:OnEventLand(EventData) -- Landing distance to carrier position. local dist=coord:Get2DDistance(self:GetCoordinate()) + -- Correct sign if necessary. if X<0 then dist=-dist end @@ -2538,7 +2539,7 @@ function AIRBOSS:OnEventLand(EventData) local w4=self:GetCoordinate():Translate(self.carrierparam.wire4, hdg):MarkToAll("Wire 4") -- Get wire - local wire=self:_GetWire(dist) + local wire=self:_GetWire(dist, 30) -- Aircraft type. local _type=EventData.IniUnit:GetTypeName() @@ -2568,7 +2569,7 @@ function AIRBOSS:OnEventLand(EventData) local dist=coord:Get2DDistance(self:GetCoordinate()) -- Get wire - local wire=self:_GetWire(dist) + local wire=self:_GetWire(dist, 0) -- Aircraft type. local _type=EventData.IniUnit:GetTypeName() @@ -2969,11 +2970,12 @@ function AIRBOSS:_GetZoneCorridor(case) c[8]=c[7]:Translate( UTILS.NMToMeters( y), radial-90) -- back along X c[9]=c[1]:Translate( UTILS.NMToMeters( 1), radial+90) -- 1 left of carrier - -- Create an array of a square! local p={} for _i,_c in ipairs(c) do - _c:SmokeBlue() + if self.Debug then + _c:SmokeBlue() + end p[_i]=_c:GetVec2() end @@ -3033,10 +3035,13 @@ function AIRBOSS:_GetHoldingZone(playerData) -- Create an array of a square! local p={} - p[1]=c1:Translate(UTILS.NMToMeters(1), hdg+90):GetVec2() --c1 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. - p[2]=c2:Translate(UTILS.NMToMeters(1), hdg+90):GetVec2() --c2 is 10 NM further behind. Also translated 1 NM starboard. - p[3]=c2:Translate(UTILS.NMToMeters(7), hdg-90):GetVec2() --p3 6 NM port of carrier. - p[4]=c1:Translate(UTILS.NMToMeters(7), hdg-90):GetVec2() --p4 6 NM port of carrier. + p[1]=c1:Translate(UTILS.NMToMeters(1), hdg-90):GetVec2() --c1 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. + p[2]=c2:Translate(UTILS.NMToMeters(1), hdg-90):GetVec2() --c2 is 10 NM further behind. Also translated 1 NM starboard. + p[3]=c2:Translate(UTILS.NMToMeters(7), hdg+90):GetVec2() --p3 6 NM port of carrier. + p[4]=c1:Translate(UTILS.NMToMeters(7), hdg+90):GetVec2() --p4 6 NM port of carrier. + + --c1:SmokeBlue() + --c2:SmokeOrange() -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. -- So stay 0-5 NM (+1 NM error margin) port of carrier. @@ -3887,24 +3892,28 @@ end --- Get wire from landing position. -- @param #AIRBOSS self -- @param #number d Distance in meters wrt carrier position where player landed. -function AIRBOSS:_GetWire(d) +-- @param #number dx Correction. +function AIRBOSS:_GetWire(d, dx) -- Little offset for the exact wire positions. - local wdx=0 - + dx=dx or 30 + -- Which wire was caught? X>0 since calculated as distance! local wire - if d wire=%d.", d, dx, d-dx, wire)) return wire end @@ -5172,18 +5181,18 @@ end --- Radio queue item. -- @type AIRBOSS.Radioitem -- @field #number Tplay Abs time when transmission should be played. --- @field #number duration Duration of the transmission in seconds. -- @field #number Tstarted Abs time when transmission began to play. -- @field #number prio Priority 0-100. -- @field #boolean isplaying Currently playing. -- @field Core.Beacon#RADIO radio Radio object. --- @field #AIRBOSS.SoundFile call --- @field #boolean loud Play loud version +-- @field #AIRBOSS.RadioSound call Radio sound. --- Check radio queue for transmissions to be broadcasted. -- @param #AIRBOSS self -- @param #table radioqueue The radio queue. -function AIRBOSS:_CheckRadioQueue(radioqueue) +-- @param #string name Name of the queue. +function AIRBOSS:_CheckRadioQueue(radioqueue, name) + --env.info(string.format("FF: check radio queue %s: n=%d", name, #radioqueue)) -- Check if queue is empty. if #radioqueue==0 then @@ -5197,7 +5206,7 @@ function AIRBOSS:_CheckRadioQueue(radioqueue) local function _sort(a, b) return (a.Tplay < b.Tplay) or (a.Tplay==b.Tplay and a.prio < b.prio) end - table.sort(radioqueue, _sort) + table.sort(radioqueue, _sort) local playing=false local next=nil --#AIRBOSS.Radioitem @@ -5212,7 +5221,7 @@ function AIRBOSS:_CheckRadioQueue(radioqueue) if transmission.isplaying then -- Check if transmission is finished. - if time>transmission.Tstarted+transmission.duration then + if time>=transmission.Tstarted+transmission.call.duration then -- Transmission over. transmission.isplaying=false @@ -5272,8 +5281,7 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay) transmission.radio=radio transmission.call=call - transmission.loud=loud - transmission.Tplay=timer.getAbsTime()+delay + transmission.Tplay=timer.getAbsTime()+(delay or 0) transmission.prio=50 transmission.isplaying=false transmission.Tstarted=nil diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 6c2b6989c..0a70c194c 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -701,7 +701,7 @@ function RESCUEHELO:onafterStatus(From, Event, To) -- Report current fuel. local text=string.format("Rescue Helo %s: state=%s fuel=%.1f", self.helo:GetName(), self:GetState(), fuel) - self:I(text) + self:T(text) -- If fuel < threshold ==> send helo to home base! if fuel Date: Fri, 30 Nov 2018 16:02:53 +0100 Subject: [PATCH 069/147] AIRBOSS v0.4.0w --- Moose Development/Moose/Ops/Airboss.lua | 2383 ++++++++++++----------- 1 file changed, 1227 insertions(+), 1156 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 5159ceaf4..e8fb6e0f3 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -7,9 +7,9 @@ -- * CASE I, II and III recoveries. -- * Supports human pilots as well as AI flight groups. -- * Automatic LSO grading. --- * Different skill levels from on-the-fly tipps for students to ziplip for pros. +-- * Different skill levels from on-the-fly tips for flight students to ziplip for pros. -- * Recovery tanker option. --- * Voice overs for LSO and AIRBOSS calls. Can easily be customized by users. +-- * Voice overs for LSO and AIRBOSS calls. -- * Automatic TACAN and ICLS channel setting. -- * Different radio channels for LSO and airboss calls. -- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels, LSO grades). @@ -219,7 +219,6 @@ AIRBOSS.CarrierType={ -- @field #number AoA Onspeed Angle of Attack. -- @field #number Dboat Ideal distance to the carrier. - --- Pattern steps. -- @type AIRBOSS.PatternStep AIRBOSS.PatternStep={ @@ -347,7 +346,6 @@ AIRBOSS.Soundfile={ } } - --- Difficulty level. -- @type AIRBOSS.Difficulty -- @field #string EASY Easy difficulty: error margin 10 for high score and 20 for low score. No score for deviation >20. @@ -420,7 +418,6 @@ AIRBOSS.GroovePos={ -- @field #number Speed Optimal speed at this point. -- @field #table Checklist Table of checklist text items to display at this point. - --- Parameters of a flight group. -- @type AIRBOSS.Flightitem -- @field Wrapper.Group#GROUP group Flight group. @@ -433,7 +430,9 @@ AIRBOSS.GroovePos={ -- @field #boolean player If true, flight is a human player. -- @field #string actype Aircraft type name. -- @field #table onboardnumbers Onboard numbers of aircraft in the group. +-- @field #number onboard Onboard number of player or fist unit in group. -- @field #number case Recovery case of flight. +-- @field #string seclead Name of section lead. -- @field #table section Other human flight groups belonging to this flight. This flight is the lead. --- Player data table holding all important parameters of each player. @@ -442,7 +441,6 @@ AIRBOSS.GroovePos={ -- @field #string name Player name. -- @field Wrapper.Client#CLIENT client Client object of player. -- @field #string callsign Callsign of player. --- @field #string onboard Onboard number. -- @field #string difficulty Difficulty level. -- @field #string step Coming pattern step. -- @field #boolean warning Set true once the player got a warning. @@ -458,24 +456,23 @@ AIRBOSS.GroovePos={ -- @field #boolean lig If true, player was long in the groove. -- @field #number Tlso Last time the LSO gave an advice. -- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. --- @field #string seclead Name of section lead. -- @field #table menu F10 radio menu -- @extends #AIRBOSS.Flightitem - --- Main radio menu. -- @field #table MenuF10 AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.0" +AIRBOSS.version="0.4.0w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Get correct wire when trapped. +-- TODO: Check distance to players during approach. PWO if too close. +-- TODO: Spin pattern. Add radio menu entry. -- TODO: Set case II and III times. -- TODO: Add radio check (LSO, AIRBOSS) to F10 radio menu. -- TODO: Add user functions. @@ -484,6 +481,7 @@ AIRBOSS.version="0.4.0" -- TODO: Foul deck check. -- TODO: Persistence of results. -- TODO: Right pattern step after bolter/wo/patternWO? +-- DONE: Get correct wire when trapped. DONE but might need further tweaking. -- DONE: Add radio transmission queue for LSO and airboss. -- TONE: CASE II. -- DONE: CASE III. @@ -590,7 +588,7 @@ function AIRBOSS:New(carriername, alias) -- TODO: Tarawa parameters. self:_InitStennis() elseif self.carriertype==AIRBOSS.CarrierType.KUZNETSOV then - -- TODO: Kusnetsov parameters - maybe... + -- Kusnetsov parameters - maybe... self:_InitStennis() else self:E(self.lid.."ERROR: Unknown carrier type!") @@ -612,6 +610,7 @@ function AIRBOSS:New(carriername, alias) self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) end + -- Init default sound files. for _name,_sound in pairs(AIRBOSS.Soundfile) do local sound=_sound --#AIRBOSS.RadioSound @@ -895,8 +894,8 @@ function AIRBOSS:onafterStart(From, Event, To) -- Events are handled my MOOSE. self:I(self.lid..string.format("Starting AIRBOSS v%s for carrier unit %s of type %s.", AIRBOSS.version, self.carrier:GetName(), self.carriertype)) - local theatre=env.mission.theatre - + -- Current map. + local theatre=env.mission.theatre self:I(self.lid..string.format("Theatre = %s", tostring(theatre))) -- Activate TACAN. @@ -1451,31 +1450,6 @@ function AIRBOSS:_CheckQueue() end end ---- Print holding queue. --- @param #AIRBOSS self --- @param #table queue Queue to print. --- @param #string name Queue name. -function AIRBOSS:_PrintQueue(queue, name) - - local text=string.format("%s Queue:", name) - if #queue==0 then - text=text.." empty." - else - for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Flightitem - local clock=UTILS.SecondsToClock(flight.time) - local stack=flight.flag:Get() - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) - local fuel=flight.group:GetFuelMin()*100 - local case=flight.case - local ai=tostring(flight.ai) - text=text..string.format("\n[%d] %s*%d: stackalt=%d ft, flag=%d, case=%d, time=%s, fuel=%d, ai=%s", - i, flight.groupname, flight.nunits, alt, stack, case, clock, fuel, ai) - end - end - self:I(self.lid..text) -end - --- Scan carrier zone for (new) units. -- @param #AIRBOSS self function AIRBOSS:_ScanCarrierZone() @@ -1534,7 +1508,7 @@ function AIRBOSS:_ScanCarrierZone() local dist=knownflight.group:GetCoordinate():Get2DDistance(self:GetCoordinate()) -- Send AI flight to marshal stack if group closes in more than 5 km and has initial flag value. - if knownflight.dist0-dist>5000 and knownflight.flag:Get()==-100 then + if knownflight.dist0-dist>UTILS.NMToMeters(5) and knownflight.flag:Get()==-100 then self:_MarshalAI(knownflight) end end @@ -1562,57 +1536,6 @@ function AIRBOSS:_ScanCarrierZone() end ---- Get onboard number of player or client. --- @param #AIRBOSS self --- @param Wrapper.Group#GROUP group Aircraft group. --- @return #string Onboard number as string. -function AIRBOSS:_GetOnboardNumberPlayer(group) - return self:_GetOnboardNumbers(group, true) -end - ---- Get onboard numbers of all units in a group. --- @param #AIRBOSS self --- @param Wrapper.Group#GROUP group Aircraft group. --- @param #boolean playeronly If true, return the onboard number for player or client skill units. --- @return #table Table of onboard numbers. -function AIRBOSS:_GetOnboardNumbers(group, playeronly) - --self:F({groupname=group:GetName}) - - -- Get group name. - local groupname=group:GetName() - - -- Debug text. - local text=string.format("Onboard numbers of group %s:", groupname) - - -- Units of template group. - local units=group:GetTemplate().units - - -- Get numbers. - local numbers={} - for _,unit in pairs(units) do - - -- Onboard number and unit name. - local n=tostring(unit.onboard_num) - local name=unit.name - local skill=unit.skill - - -- Debug text. - text=text..string.format("\n- unit %s: onboard #=%s skill=%s", name, n, skill) - - if playeronly and skill=="Client" or skill=="Player" then - -- There can be only one player in the group. so we skill everything else - return n - end - - -- Table entry. - numbers[name]=n - end - - -- Debug info. - self:I(self.lid..text) - - return numbers -end --- Orbit at a specified position at a specified alititude with a specified speed. -- @param #AIRBOSS self @@ -1660,7 +1583,6 @@ function AIRBOSS:_MarshalAI(flight) local groupname=flight.groupname -- Check that we do not add a recovery tanker for marshaling. - -- TODO: Fix group name. if self.tanker and self.tanker.tanker:GetName()==groupname then return end @@ -1790,56 +1712,8 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) return altitude, p1, p2 end ---- Get next free stack. --- @param #AIRBOSS self --- @param #number case Recovery case --- @return #number Smalest (lowest) free stack. -function AIRBOSS:_GetFreeStack(case) - - case=case or self.case - - local stack - if case==1 then - stack=self:_GetQueueInfo(self.Qmarshal, 1) - else - stack=self:_GetQueueInfo(self.Qmarshal, 23) - end - - return stack+1 -end ---- Get number of groups and units in queue. --- @param #AIRBOSS self --- @param #table queue The queue. Can me all, marshal or pattern. --- @param #number case (Optional) Count only flights which are in a specific recovery case. --- @return #number Total Number of flight groups in queue. --- @return #number Total number of aircraft in queue. -function AIRBOSS:_GetQueueInfo(queue, case) - - local ngroup=0 - local nunits=0 - - for _,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Flightitem - - if case then - - if flight.case==case or (case==23 and (flight.case==2 or flight.case==3)) then - ngroup=ngroup+1 - nunits=nunits+flight.nunits - end - - else - ngroup=ngroup+1 - nunits=nunits+flight.nunits - end - - end - - return nunits, ngroup -end - --- Add a flight group to the marshal stack. -- @param #AIRBOSS self -- @param #AIRBOSS.Flightitem flight Flight group. @@ -1888,11 +1762,20 @@ function AIRBOSS:_CheckCollapseMarshalStack(flight) self:_CollapseMarshalStack(flight) else -- TODO only if skil is not TOPGUN - text=text..string.format("\nUse F10 radio menu \"Commence!\" command when you are ready!") + --text=text.. end -- TODO: Message to all players! - MESSAGE:New(text, 15, "MARSHAL"):ToAll() + MESSAGE:New(text, 15, "MARSHAL"):ToAll() + --self:MessageToAll(text, "MARSHAL", flight) + + -- Hint for human players. + if not flight.ai then + local playerData=flight --#AIRBOSS.PlayerData + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + self:MessageToPlayer(flight, string.format("\nUse F10 radio menu \"Commence!\" command when you are ready!"), nil, "", 5) + end + end end --- Collapse marshal stack. @@ -1976,6 +1859,81 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) end end +--- Get next free stack. +-- @param #AIRBOSS self +-- @param #number case Recovery case +-- @return #number Smalest (lowest) free stack. +function AIRBOSS:_GetFreeStack(case) + + case=case or self.case + + local stack + if case==1 then + stack=self:_GetQueueInfo(self.Qmarshal, 1) + else + stack=self:_GetQueueInfo(self.Qmarshal, 23) + end + + return stack+1 +end + + +--- Get number of groups and units in queue. +-- @param #AIRBOSS self +-- @param #table queue The queue. Can me all, marshal or pattern. +-- @param #number case (Optional) Count only flights which are in a specific recovery case. +-- @return #number Total Number of flight groups in queue. +-- @return #number Total number of aircraft in queue. +function AIRBOSS:_GetQueueInfo(queue, case) + + local ngroup=0 + local nunits=0 + + for _,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.Flightitem + + if case then + + if flight.case==case or (case==23 and (flight.case==2 or flight.case==3)) then + ngroup=ngroup+1 + nunits=nunits+flight.nunits + end + + else + ngroup=ngroup+1 + nunits=nunits+flight.nunits + end + + end + + return nunits, ngroup +end + +--- Print holding queue. +-- @param #AIRBOSS self +-- @param #table queue Queue to print. +-- @param #string name Queue name. +function AIRBOSS:_PrintQueue(queue, name) + + local text=string.format("%s Queue:", name) + if #queue==0 then + text=text.." empty." + else + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.Flightitem + local clock=UTILS.SecondsToClock(flight.time) + local stack=flight.flag:Get() + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) + local fuel=flight.group:GetFuelMin()*100 + local case=flight.case + local ai=tostring(flight.ai) + text=text..string.format("\n[%d] %s*%d: stackalt=%d ft, flag=%d, case=%d, time=%s, fuel=%d, ai=%s", + i, flight.groupname, flight.nunits, alt, stack, case, clock, fuel, ai) + end + end + self:I(self.lid..text) +end + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FLIGHT & PLAYER functions ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1992,11 +1950,12 @@ function AIRBOSS:_CreateFlightGroup(group) -- New flight. local flight={} --#AIRBOSS.Flightitem - if not self:_InQueue(self.flights,group) then + -- Check if not already in flights + if not self:_InQueue(self.flights, group) then -- Flight group name local groupname=group:GetName() - local human=self:_IsHuman(group) + local human, playername=self:_IsHuman(group) -- Queue table item. flight.group=group @@ -2009,12 +1968,20 @@ function AIRBOSS:_CreateFlightGroup(group) flight.ai=not human flight.actype=group:GetTypeName() flight.onboardnumbers=self:_GetOnboardNumbers(group) + flight.seclead=flight.group:GetUnit(1):GetName() -- Sec lead is first unitname of group but player name for players. flight.section={} - -- TODO set elsewhere. + -- Note, this should be set elsewhere. flight.case=self.case + + -- Onboard + if flight.ai then + flight.onboard=flight.onboardnumbers[flight.seclead] + else + flight.onboard=self:_GetOnboardNumberPlayer(group) + end - -- Add to known flights inside CCA zone. + -- Add to known flights. table.insert(self.flights, flight) else @@ -2048,10 +2015,8 @@ function AIRBOSS:_NewPlayer(unitname) -- Player unit, client and callsign. playerData.unit = playerunit playerData.name = playername - playerData.group = playerunit:GetGroup() playerData.callsign = playerData.unit:GetCallsign() playerData.client = CLIENT:FindByName(unitname, nil, true) - playerData.onboard = self:_GetOnboardNumberPlayer(playerData.group) playerData.seclead = playername -- Number of passes done by player. @@ -2095,8 +2060,9 @@ function AIRBOSS:_InitPlayer(playerData) playerData.landed=false playerData.Tlso=timer.getTime() - if playerData.group:GetName():match("Groove") then - self:MessageToPlayer(playerData, "Group name contains Groove. You are supposed to test the groove.") + -- Set us up on final if group name contains "Groove". But only for the first pass. + if playerData.group:GetName():match("Groove") and playerData.passes==0 then + self:MessageToPlayer(playerData, "Group name contains \"Groove\". Happy groove testing.") playerData.step=AIRBOSS.PatternStep.FINAL end @@ -2131,7 +2097,7 @@ end --- Check if a group is in a queue. -- @param #AIRBOSS self -- @param #table queue The queue to check. --- @param Wrapper.Group#GROUP group +-- @param Wrapper.Group#GROUP group The group to be checked. -- @return #boolean If true, group is in the queue. False otherwise. function AIRBOSS:_InQueue(queue, group) local name=group:GetName() @@ -2391,8 +2357,8 @@ function AIRBOSS:_CheckPlayerStatus() elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then - -- Debriefing in 10 seconds. - SCHEDULER:New(nil, self._Debrief, {self, playerData}, 10) + -- Debriefing in 5 seconds. + SCHEDULER:New(nil, self._Debrief, {self, playerData}, 5) -- Undefined status. playerData.step=AIRBOSS.PatternStep.UNDEFINED @@ -2404,7 +2370,6 @@ function AIRBOSS:_CheckPlayerStatus() end else - --playerData.inbigzone=false self:E(self.lid.."WARNING: Player left the CCA!") end @@ -2513,11 +2478,7 @@ function AIRBOSS:OnEventLand(EventData) -- Coordinate at landing event local coord=playerData.unit:GetCoordinate() - - -- Debug mark of player landing coord. - local lp=coord:MarkToAll("Landing coord.") - coord:SmokeGreen() - + -- Get distances relative to local X,Z,rho,phi=self:_GetDistances(_unit) @@ -2529,14 +2490,20 @@ function AIRBOSS:OnEventLand(EventData) dist=-dist end - -- TODO: check if 360 degrees correctino is necessary! - local hdg=self.carrier:GetHeading()+self.carrierparam.rwyangle - - -- Debug marks of wires. - local w1=self:GetCoordinate():Translate(self.carrierparam.wire1, hdg):MarkToAll("Wire 1") - local w2=self:GetCoordinate():Translate(self.carrierparam.wire2, hdg):MarkToAll("Wire 2") - local w3=self:GetCoordinate():Translate(self.carrierparam.wire3, hdg):MarkToAll("Wire 3") - local w4=self:GetCoordinate():Translate(self.carrierparam.wire4, hdg):MarkToAll("Wire 4") + -- Debug output + if self.Debug then + local hdg=self.carrier:GetHeading()+self.carrierparam.rwyangle + + -- Debug marks of wires. + local w1=self:GetCoordinate():Translate(self.carrierparam.wire1, hdg):MarkToAll("Wire 1") + local w2=self:GetCoordinate():Translate(self.carrierparam.wire2, hdg):MarkToAll("Wire 2") + local w3=self:GetCoordinate():Translate(self.carrierparam.wire3, hdg):MarkToAll("Wire 3") + local w4=self:GetCoordinate():Translate(self.carrierparam.wire4, hdg):MarkToAll("Wire 4") + + -- Debug mark of player landing coord. + local lp=coord:MarkToAll("Landing coord.") + coord:SmokeGreen() + end -- Get wire local wire=self:_GetWire(dist, 30) @@ -2545,7 +2512,7 @@ function AIRBOSS:OnEventLand(EventData) local _type=EventData.IniUnit:GetTypeName() -- Debug text. - local text=string.format("Player %s of type %s landed at dist=%.1f m. Trapped wire=%d.", EventData.IniUnitName, _type, dist, wire) + local text=string.format("Player %s AC type %s landed at dist=%.1f m. Trapped wire=%d.", EventData.IniUnitName, _type, dist, wire) text=text..string.format("X=%.1f m, Z=%.1f m, rho=%.1f m, phi=%.1f deg.", X, Z, rho, phi) self:I(self.lid..text) @@ -2593,9 +2560,9 @@ function AIRBOSS:OnEventCrash(EventData) local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - self:I(self.lid.."CRASH: unit = "..tostring(EventData.IniUnitName)) - self:I(self.lid.."CRASH: group = "..tostring(EventData.IniGroupName)) - self:I(self.lid.."CARSH: player = "..tostring(_playername)) + self:T3(self.lid.."CRASH: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."CRASH: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."CARSH: player = "..tostring(_playername)) if _unit and _playername then self:I(self.lid..string.format("Player %s crashed!",_playername)) @@ -2616,9 +2583,9 @@ function AIRBOSS:OnEventEjection(EventData) local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - self:I(self.lid.."EJECT: unit = "..tostring(EventData.IniUnitName)) - self:I(self.lid.."EJECT: group = "..tostring(EventData.IniGroupName)) - self:I(self.lid.."EJECT: player = "..tostring(_playername)) + self:T3(self.lid.."EJECT: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."EJECT: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."EJECT: player = "..tostring(_playername)) if _unit and _playername then self:I(self.lid..string.format("Player %s ejected!",_playername)) @@ -2634,7 +2601,6 @@ end -- PATTERN functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - --- Holding. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. @@ -2653,7 +2619,7 @@ function AIRBOSS:_Holding(playerData) local playeralt=unit:GetAltitude() -- Get holding zone of player. - local zoneHolding=self:_GetHoldingZone(playerData) + local zoneHolding=self:_GetZoneHolding(playerData.case, playerData.flag:Get()) -- Check if player is in holding zone. local inholdingzone=unit:IsInZone(zoneHolding) @@ -2722,6 +2688,917 @@ function AIRBOSS:_Holding(playerData) self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 5) end + +--- Commence approach. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Commencing(playerData) + + -- Initialize player data for new approach. + self:_InitPlayer(playerData) + + -- Commence + local text=string.format("Commencing. (Case %d)", self.case) + + -- Message to all players. + self:MessageToAll(text, playerData.onboard, "", 5) + + -- Next step: depends on case recovery. + if self.case==1 then + -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. + playerData.step=AIRBOSS.PatternStep.INITIAL + else + -- CASE II/III: Player has to start the descent at 4000 ft/min to the platform at 5k ft. + playerData.step=AIRBOSS.PatternStep.PLATFORM + end +end + +--- Start pattern when player enters the initial zone in case I/II recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Initial(playerData) + + -- Check if player is in initial zone and entering the CASE I pattern. + if playerData.unit:IsInZone(self.zoneInitial) then + + -- Inform player. + local hint=string.format("Entering the pattern.") + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint.."\nAim for 800 feet and 350 kts at the break entry." + end + + -- Send message. + self:MessageToPlayer(playerData, hint, "MARSHAL") + + -- Next step: upwind. + playerData.step=AIRBOSS.PatternStep.UPWIND + end + +end + +--- Platform at 5k ft for case II/III recoveries. Descent at 2000 ft/min. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Platform(playerData) + + -- Check if player is in valid zone + local validzone=self:_GetZoneCorridor(playerData.case) + + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and not playerData.warning then + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") + playerData.warning=true + end + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZonePlatform(playerData.case)) + + -- Check if we are in zone. + if inzone then + + -- Debug message. + MESSAGE:New("Platform step reached", 5):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed =self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, altitude) + + -- Get altitude hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: depends. + if math.abs(self.holdingoffset)>0 then + -- Turn to BRC (case II) or FB (case III). + playerData.step=AIRBOSS.PatternStep.ARCIN + else + if playerData.case==2 then + -- Case II: Initial zone then Case I recovery. + playerData.step=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- CASE III: Dirty up. + playerData.step=AIRBOSS.PatternStep.DIRTYUP + end + end + playerData.warning=nil + end +end + + +--- Arc in turn for case II/III recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_ArcInTurn(playerData) + + -- Check if player is in valid zone + local validzone=self:_GetZoneCorridor(playerData.case) + + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and not playerData.warning then + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") + playerData.warning=true + end + + -- Back in zone. + -- TODO: add this to the other checkpoints! + if not invalid and playerData.warning then + self:MessageToPlayer(playerData, "You are back in the approach corridor. Now stay there!", "AIRBOSS") + playerData.warning=false + end + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneArcIn(playerData.case)) + + if inzone then + + -- Debug message. + MESSAGE:New("Arc Turn In step reached", 5):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get speed hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s", playerData.step, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: Arc Out Turn. + playerData.step=AIRBOSS.PatternStep.ARCOUT + + playerData.warning=nil + end +end + +--- Arc out turn for case II/III recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_ArcOutTurn(playerData) + + -- Check if player is in valid zone + local validzone=self:_GetZoneCorridor(playerData.case) + + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and not playerData.warning then + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") + playerData.warning=true + end + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneArcOut(playerData.case)) + + --if self:_CheckLimits(X, Z, self.DirtyUp) then + if inzone then + + -- Debug message. + MESSAGE:New("Arc Turn Out step reached", 5):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get speed hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s", playerData.step, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: + if playerData.case==2 then + -- Case II: Initial. + playerData.step=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- Case III: Dirty up. + playerData.step=AIRBOSS.PatternStep.DIRTYUP + else + -- ERROR! + end + + playerData.warning=nil + end +end + +--- Dirty up and level out at 1200 ft for case III recovery. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_DirtyUp(playerData) + + -- Check if player is in valid zone + local validzone=self:_GetZoneCorridor(playerData.case) + + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and not playerData.warning then + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") + playerData.warning=true + end + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) + + --if self:_CheckLimits(X, Z, self.DirtyUp) then + if inzone then + + -- Debug message. + MESSAGE:New("Dirty up step reached", 5):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt, debrief=self:_AltitudeCheck(playerData, altitude) + + -- Get speed hint. + -- TODO: Not sure if we already need to be onspeed AoA at this point? + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). + playerData.step=AIRBOSS.PatternStep.BULLSEYE + + playerData.warning=nil + end +end + +--- Intercept glide slop and follow ICLS, aka Bullseye for case III recovery. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Bullseye(playerData) + + -- Check if player is in valid zone + local validzone=self:_GetZoneCorridor(playerData.case) + + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and not playerData.warning then + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") + playerData.warning=true + end + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) + + -- Check that we reached the position. + --if self:_CheckLimits(X, Z, self.Bullseye) then + if inzone then + + -- Debug message. + MESSAGE:New("Bullseye step reached", 5):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, altitude) + + -- Get altitude hint. + local hintAoA=self:_AoACheck(playerData, aoa) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: Groove Call the ball. + playerData.step=AIRBOSS.PatternStep.GROOVE_XX + + playerData.warning=nil + end +end + + +--- Upwind leg or break entry for case I/II recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Upwind(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Abort condition check. + if self:_CheckAbort(X, Z, self.Upwind) then + self:_AbortPattern(playerData, X, Z, self.Upwind, true) + return + end + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, self.Upwind) then + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, alt) + + -- Get speed hint. + local hintSpeed=self:_AltitudeCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Debrief. + --self:_AddToDebrief(playerData, debrief) + + -- Next step: Early Break. + playerData.step=AIRBOSS.PatternStep.EARLYBREAK + end +end + + +--- Break. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #string part Part of the break. +function AIRBOSS:_Break(playerData, part) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Early or late break. + local breakpoint = self.BreakEarly + if part=="late" then + breakpoint = self.BreakLate + end + + -- Check abort conditions. + if self:_CheckAbort(X, Z, breakpoint) then + self:_AbortPattern(playerData, X, Z, breakpoint, true) + return + end + + -- Check limits. + if self:_CheckLimits(X, Z, breakpoint) then + + -- Get optimal altitude, distance and speed. + local altitude=self:_GetAircraftParameters(playerData) + + -- Grade altitude. + local hint, debrief=self:_AltitudeCheck(playerData, altitude) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s %s", playerData.step, hint) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Debrief + self:_AddToDebrief(playerData, debrief) + + -- Next step: Late Break or Abeam. + if part=="early" then + playerData.step=AIRBOSS.PatternStep.LATEBREAK + else + playerData.step=AIRBOSS.PatternStep.ABEAM + end + end +end + +--- Long downwind leg check. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_CheckForLongDownwind(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z=self:_GetDistances(playerData.unit) + + -- One NM from carrier is too far. + local limit=UTILS.NMToMeters(-1.5) + + -- Check we are not too far out w.r.t back of the boat. + if X90 and self:_CheckLimits(X, Z, self.Wake) then + -- Message to player. + self:MessageToPlayer(playerData, "You are already at the wake and have not passed the 90. Turn faster next time!", "LSO") + --TODO: pattern WO? + end +end + +--- At the Wake. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Wake(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z = self:_GetDistances(playerData.unit) + + -- Check abort conditions. + if self:_CheckAbort(X, Z, self.Wake) then + self:_AbortPattern(playerData, X, Z, self.Wake, true) + return + end + + -- Right behind the wake of the carrier dZ>0. + if self:_CheckLimits(X, Z, self.Wake) then + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Grade altitude. + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) + + -- Grade AoA. + local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + self:MessageToPlayer(playerData, hint, "LSO", "") + end + + -- Debrief. + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) + + -- Add to debrief. + self:_AddToDebrief(playerData, debrief) + + -- Next step: Final. + playerData.step=AIRBOSS.PatternStep.FINAL + end +end + +--- Turn to final. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Final(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi = self:_GetDistances(playerData.unit) + + -- In front of carrier or more than 4 km behind carrier. + if self:_CheckAbort(X, Z, self.Groove) then + self:_AbortPattern(playerData, X, Z, self.Groove, true) + return + end + + -- Relative heading 0=fly parallel +-90=fly perpendicular + local relhead=self:_GetRelativeHeading(playerData.unit, true) + + -- Line up wrt runway. + local lineup=self:_Lineup(playerData, true) + + -- Player's angle of bank. + local roll=playerData.unit:GetRoll() + + -- Check if player is in +-5 deg cone and flying towards the runway. + if math.abs(lineup)<5 then --and math.abs(relhead)<5 then + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Grade altitude. + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) + + -- AoA feed back + local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + self:MessageToPlayer(playerData, hint, "LSO", "") + end + + -- Add to debrief. + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) + self:_AddToDebrief(playerData, debrief) + + -- Gather pilot data. + local groovedata={} --#AIRBOSS.GrooveData + groovedata.Step=playerData.step + groovedata.Alt=alt + groovedata.AoA=aoa + groovedata.GSE=self:_Glideslope(playerData, 3.5) + groovedata.LUE=self:_Lineup(playerData, true) + groovedata.Roll=roll + groovedata.Rhdg=relhead + + -- TODO: could add angled approach if lineup<5 and relhead>5. This would mean the player has not tunred in correctly! + + -- Groove + playerData.groove.X0=groovedata + + -- Next step: X start & call the ball. + playerData.step=AIRBOSS.PatternStep.GROOVE_XX + end + +end + + +--- In the groove. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Groove(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi = self:_GetDistances(playerData.unit) + + -- Player altitude + local alt=playerData.unit:GetAltitude() + + -- Player group. + local player=playerData.unit:GetGroup() + + -- Check abort conditions. + if self:_CheckAbort(X, Z, self.Trap) then + self:_AbortPattern(playerData, X, Z, self.Trap, true) + return + end + + -- Lineup with runway centerline. + local lineupError=self:_Lineup(playerData, true) + + -- Glide slope. + local glideslopeError=self:_Glideslope(playerData, 3.5) + + -- Get AoA. + local AoA=playerData.unit:GetAoA() + + -- Ranges in the groove. + local RXX=UTILS.NMToMeters(0.750)+math.abs(self.carrierparam.sterndist) -- Start of groove. 0.75 = 1389 m + local RRB=UTILS.NMToMeters(0.500)+math.abs(self.carrierparam.sterndist) -- Roger Ball! call. 0.5 = 926 m + local RIM=UTILS.NMToMeters(0.375)+math.abs(self.carrierparam.sterndist) -- In the Middle 0.75/2. 0.375 = 695 m + local RIC=UTILS.NMToMeters(0.100)+math.abs(self.carrierparam.sterndist) -- In Close. 0.1 = 185 m + local RAR=UTILS.NMToMeters(0.000)+math.abs(self.carrierparam.sterndist) -- At the Ramp. + + -- Data + local groovedata={} --#AIRBOSS.GrooveData + groovedata.Step=playerData.step + groovedata.Alt=alt + groovedata.AoA=AoA + groovedata.GSE=glideslopeError + groovedata.LUE=lineupError + groovedata.Roll=playerData.unit:GetRoll() + groovedata.Rhdg=self:_GetRelativeHeading(playerData.unit, true) + + if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX then + + -- LSO "Call the ball" call. + self:RadioTransmission(self.LSOradio, self.radiocall.CALLTHEBALL) + playerData.Tlso=timer.getTime() + + -- Store data. + playerData.groove.XX=groovedata + + -- Next step: roger ball. + playerData.step=AIRBOSS.PatternStep.GROOVE_RB + + elseif rho<=RRB and playerData.step==AIRBOSS.PatternStep.GROOVE_RB then + + -- Pilot: "Roger ball" call. + self:RadioTransmission(self.LSOradio, self.radiocall.ROGERBALL) + playerData.Tlso=timer.getTime()+1 + + -- Store data. + playerData.groove.RB=groovedata + + -- Next step: in the middle. + playerData.step=AIRBOSS.PatternStep.GROOVE_IM + + elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then + + -- Debug. + local text=string.format("FF IM=%d", rho) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:I(self.lid..string.format("FF IM=%d", rho)) + + -- Store data. + playerData.groove.IM=groovedata + + -- Next step: in close. + playerData.step=AIRBOSS.PatternStep.GROOVE_IC + + elseif rho<=RIC and playerData.step==AIRBOSS.PatternStep.GROOVE_IC then + + -- Check if player was already waved off. + if playerData.waveoff==false then + + -- Debug + local text=string.format("FF IC=%d", rho) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Store data. + playerData.groove.IC=groovedata + + -- Check if player should wave off. + local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData.difficulty) + + -- Let's see.. + if waveoff then + + -- LSO Wave off! + self:RadioTransmission(self.LSOradio, self.radiocall.WAVEOFF) + playerData.Tlso=timer.getTime() + + -- Player was waved off! + playerData.waveoff=true + + return + else + -- Next step: AR at the ramp. + playerData.step=AIRBOSS.PatternStep.GROOVE_AR + end + + end + + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AR then + + -- Debug. + local text=string.format("FF AR=%d", rho) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Store data. + playerData.groove.AR=groovedata + + -- Next step: in the wires. + playerData.step=AIRBOSS.PatternStep.GROOVE_IW + end + + -- Time since last LSO call. + local time=timer.getTime() + local deltaT=time-playerData.Tlso + + -- Check if we are beween 3/4 NM and end of ship. + if X<0 and rho>=RAR and rho=3 and playerData.waveoff==false then + + -- LSO call if necessary. + self:_LSOadvice(playerData, glideslopeError, lineupError) + + elseif X>100 then + + if playerData.landed then + + -- Add to debrief. + if playerData.waveoff then + self:_AddToDebrief(playerData, "You were waved off but landed anyway. Airboss wants to talk to you!") + else + self:_AddToDebrief(playerData, "You boltered.") + end + + else + + -- Add to debrief. + self:_AddToDebrief(playerData, "You were waved off.") + + -- Next step: debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + + end + end +end + +--- LSO check if player needs to wave off. +-- Wave off conditions are: +-- +-- * Glide slope error > 3 degrees. +-- * Line up error > 3 degrees. +-- * AoA<6.9 or AoA>9.3. +-- @param #AIRBOSS self +-- @param #number glideslopeError Glide slope error in degrees. +-- @param #number lineupError Line up error in degrees. +-- @param #number AoA Angle of attack of player aircraft. +-- @param #string diffifulty Difficulty setting of player. +-- @return #boolean If true, player should wave off! +function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, difficulty) + + local waveoff=false + + -- Too high or too low? + if math.abs(glideslopeError)>1 then + self:I(self.lid.."Wave off due to glide slope error >1 degree!") + waveoff=true + end + + -- Too far from centerline? + if math.abs(lineupError)>3 then + self:I(self.lid.."Wave off due to line up error >3 degrees!") + waveoff=true + end + + -- Too slow or too fast? + if AoA<6.9 or AoA>9.3 then + if difficulty==AIRBOSS.Difficulty.HARD then + self:I(self.lid.."Wave off due to AoA<6.9 or AoA>9.3!") + waveoff=true + end + end + + return waveoff +end + +--- Get wire from landing position. +-- @param #AIRBOSS self +-- @param #number d Distance in meters wrt carrier position where player landed. +-- @param #number dx Correction. +function AIRBOSS:_GetWire(d, dx) + + -- Little offset for the exact wire positions. + dx=dx or 30 + + -- Which wire was caught? X>0 since calculated as distance! + local wire + if d-dx wire=%d.", d, dx, d-dx, wire)) + + return wire +end + +--- Trapped? +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number wire The wire caught. +function AIRBOSS:_Trapped(playerData, wire) + + if playerData.unit:InAir()==false then + -- Seems we have successfully landed. + + -- Message to player. + local text=string.format("Trapped %d-wire.", wire) + if wire==3 then + text=text.." Well done!" + elseif wire==2 then + text=text.." Not bad, maybe you even get the 3rd next time." + elseif wire==4 then + text=text.." That was scary. You can do better than this!" + elseif wire==1 then + text=text.." Try harder next time!" + end + self:MessageToPlayer(playerData, text, "LSO", "") + + -- Debrief. + local hint = string.format("Trapped %d-wire.", wire) + self:_AddToDebrief(playerData, hint, "Goove: IW") + + else + --Still in air ==> Boltered! + playerData.boltered=true + end + + -- Next step: debriefing. + playerData.step=AIRBOSS.PatternStep.DEBRIEF +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ZONE functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Get Bullseye zone with radius 1 NM and DME 3 NM from the carrier. Radial depends on recovery case. -- @param #AIRBOSS self -- @param #number case Recovery case. @@ -2946,29 +3823,36 @@ function AIRBOSS:_GetZoneCorridor(case) self:I(string.format("FF case %d offset = %d", case, offset)) -- Width of the box. - local w=UTILS.NMToMeters(5) + local w=UTILS.NMToMeters(2) -- Length of the box. local l=UTILS.NMToMeters(10) -- Angle between radial and offset in rad. local alpha=math.rad(self.holdingoffset) - -- Distance from ArcIn to ArcOut zone + -- Distance from carrier to arc zone. local d=12 + + -- Distance from ArcIn to ArcOut zone local y=d*math.tan(alpha) - local d2=math.cos(alpha)/2 + --local d2=math.cos(alpha)/2 + + -- Get the extra bit we need to go back from the end to the arc turn in. + local C=w/math.cos(alpha) + local b=w*math.tan(alpha) + local a=C-b local c={} c[1]=self:GetCoordinate() -- Carrier coordinate - c[2]=c[1]:Translate( UTILS.NMToMeters( 1), radial-90) -- 1 Right of carrier - c[3]=c[2]:Translate( UTILS.NMToMeters(13), radial) -- 1 Right and 13 "south" - c[4]=c[3]:Translate( UTILS.NMToMeters( y), radial+90) -- y left, 13 south - c[5]=c[4]:Translate( UTILS.NMToMeters(10), offset) -- to back wall angled - c[6]=c[5]:Translate( UTILS.NMToMeters( 2), offset+90) -- Back wall (angled) - c[7]=c[6]:Translate(-UTILS.NMToMeters(10+d2), offset) -- back along X & Z - c[8]=c[7]:Translate( UTILS.NMToMeters( y), radial-90) -- back along X - c[9]=c[1]:Translate( UTILS.NMToMeters( 1), radial+90) -- 1 left of carrier + c[2]=c[1]:Translate( UTILS.NMToMeters(w/2), radial-90) -- 1 Right of carrier + c[3]=c[2]:Translate( UTILS.NMToMeters(d+w/2), radial) -- 13 "south" @ 1 right + c[4]=c[3]:Translate( UTILS.NMToMeters(y), radial+90) -- y left @ 13 south + c[5]=c[4]:Translate( UTILS.NMToMeters(l), offset) -- 10 NM to back wall (angled) + c[6]=c[5]:Translate( UTILS.NMToMeters(w), offset+90) -- Back wall (angled) + c[7]=c[6]:Translate(-UTILS.NMToMeters(l+a), offset) -- 10+a Back along X & Z + c[8]=c[7]:Translate( UTILS.NMToMeters(y), radial-90) -- Back along X + c[9]=c[1]:Translate( UTILS.NMToMeters(w/2), radial+90) -- 1 left of carrier -- Create an array of a square! local p={} @@ -2988,962 +3872,62 @@ end --- Get holding zone of player. -- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number case Recovery case. +-- @param #number stack Marshal stack number. -- @return Core.Zone#ZONE Holding zone. -function AIRBOSS:_GetHoldingZone(playerData) +function AIRBOSS:_GetZoneHolding(case, stack) - --TODO: make indepened of whole playerData. Just use case and stack as input! - - -- Player unit and flight. - local unit=playerData.unit - - -- Create a holding zone depending on recovery case. + -- Holding zone. local zoneHolding=nil --Core.Zone#ZONE + + -- Stack is <= 0 ==> no marshal zone. + if stack<=0 then + return nil + end - if unit:IsAlive() then + -- Pattern alitude. + local patternalt, c1, c2=self:_GetMarshalAltitude(stack, case) - -- Current stack. - local stack=playerData.flag:Get() + if case==1 then + -- CASE I - -- Player's recovery case. - local case=playerData.case + -- Zone 2.5 NM port of carrier with a radius of 3 NM (holding pattern should be < 5 NM). + --zoneHolding=ZONE_UNIT:New("CASE I Holding Zone", self.carrier, UTILS.NMToMeters(3), {dx=0, dy=-UTILS.NMToMeters(2.5), relative_to_unit=true}) - -- Stack is <= 0 ==> no marshal zone. - if stack<=0 then - return nil - end + local R=UTILS.MetersToNM(2.5) + local coord=self:GetCoordinate():Translate(R, 270) - -- Pattern alitude. - local patternalt, c1, c2=self:_GetMarshalAltitude(stack, case) - - if case==1 then - -- CASE I - - -- Zone 2.5 NM port of carrier with a radius of 3 NM (holding pattern should be < 5 NM). - zoneHolding=ZONE_UNIT:New("CASE I Holding Zone", self.carrier, UTILS.NMToMeters(3), {dx=0, dy=-UTILS.NMToMeters(2.5), relative_to_unit=true}) - - else - -- CASE II/II - - -- Get radial. - local hdg - if case==2 then - hdg=self:GetRadialCase2(false, true) - else - hdg=self:GetRadialCase3(false, true) - end - - -- Create an array of a square! - local p={} - p[1]=c1:Translate(UTILS.NMToMeters(1), hdg-90):GetVec2() --c1 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. - p[2]=c2:Translate(UTILS.NMToMeters(1), hdg-90):GetVec2() --c2 is 10 NM further behind. Also translated 1 NM starboard. - p[3]=c2:Translate(UTILS.NMToMeters(7), hdg+90):GetVec2() --p3 6 NM port of carrier. - p[4]=c1:Translate(UTILS.NMToMeters(7), hdg+90):GetVec2() --p4 6 NM port of carrier. - - --c1:SmokeBlue() - --c2:SmokeOrange() - - -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. - -- So stay 0-5 NM (+1 NM error margin) port of carrier. - zoneHolding=ZONE_POLYGON_BASE:New("CASE II/III Holding Zone", p) + zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", coord:GetVec2(), R) + + else + -- CASE II/II + + -- Get radial. + local hdg + if case==2 then + hdg=self:GetRadialCase2(false, true) + else + hdg=self:GetRadialCase3(false, true) end + + -- Create an array of a square! + local p={} + p[1]=c1:Translate(UTILS.NMToMeters(1), hdg-90):GetVec2() --c1 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. + p[2]=c2:Translate(UTILS.NMToMeters(1), hdg-90):GetVec2() --c2 is 10 NM further behind. Also translated 1 NM starboard. + p[3]=c2:Translate(UTILS.NMToMeters(7), hdg+90):GetVec2() --p3 6 NM port of carrier. + p[4]=c1:Translate(UTILS.NMToMeters(7), hdg+90):GetVec2() --p4 6 NM port of carrier. + + --c1:SmokeBlue() + --c2:SmokeOrange() + + -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. + -- So stay 0-5 NM (+1 NM error margin) port of carrier. + zoneHolding=ZONE_POLYGON_BASE:New("CASE II/III Holding Zone", p) end return zoneHolding end ---- Commence approach. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_Commencing(playerData) - - -- Initialize player data for new approach. - self:_InitPlayer(playerData) - - -- Commence - local text=string.format("Commencing. (Case %d)", self.case) - - -- Message to all players. - self:MessageToAll(text, playerData.onboard, "", 5) - - -- Next step: depends on case recovery. - if self.case==1 then - -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. - playerData.step=AIRBOSS.PatternStep.INITIAL - else - -- CASE II/III: Player has to start the descent at 4000 ft/min. - playerData.step=AIRBOSS.PatternStep.PLATFORM - end -end - ---- Start pattern when player enters the initial zone in case I/II recoveries. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Initial(playerData) - - -- Check if player is in initial zone and entering the CASE I pattern. - if playerData.unit:IsInZone(self.zoneInitial) then - - -- Inform player. - local hint=string.format("Entering the pattern.") - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - hint=hint.."\nAim for 800 feet and 350 kts at the break entry." - end - - -- Send message. - self:MessageToPlayer(playerData, hint, "MARSHAL") - - -- Next step: upwind. - playerData.step=AIRBOSS.PatternStep.UPWIND - end - -end - ---- Platform at 5k ft for case II/III recoveries. Descent at 2000 ft/min. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Platform(playerData) - - -- Check if player is in valid zone - local validzone=self:_GetZoneCorridor(playerData.case) - - -- Check if we are inside the moving zone. - local invalid=playerData.unit:IsNotInZone(validzone) - - -- Issue warning. - if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") - playerData.warning=true - end - - -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self:_GetZonePlatform(playerData.case)) - - -- Check if we are in zone. - if inzone then - - -- Debug message. - MESSAGE:New("Platform step reached", 5):ToAllIf(self.Debug) - - -- Get optimal altitiude. - local altitude, aoa, distance, speed =self:_GetAircraftParameters(playerData) - - -- Get altitude hint. - local hintAlt=self:_AltitudeCheck(playerData, altitude) - - -- Get altitude hint. - local hintSpeed=self:_SpeedCheck(playerData, speed) - - -- Message to player. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) - self:MessageToPlayer(playerData, hint, "MARSHAL", "") - end - - -- Next step: depends. - if math.abs(self.holdingoffset)>0 then - -- Turn to BRC (case I) or FB (case III). - playerData.step=AIRBOSS.PatternStep.ARCIN - else - if playerData.case==2 then - -- Case II: Initial zone then Case I recovery. - playerData.step=AIRBOSS.PatternStep.INITIAL - elseif playerData.case==3 then - -- CASE III: Dirty up. - playerData.step=AIRBOSS.PatternStep.DIRTYUP - end - end - playerData.warning=nil - end -end - - ---- Arc in turn for case II/III recoveries. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_ArcInTurn(playerData) - - -- Check if player is in valid zone - local validzone=self:_GetZoneCorridor(playerData.case) - - -- Check if we are inside the moving zone. - local invalid=playerData.unit:IsNotInZone(validzone) - - -- Issue warning. - if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") - playerData.warning=true - end - - -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self:_GetZoneArcIn(playerData.case)) - - if inzone then - - -- Debug message. - MESSAGE:New("Arc Turn In step reached", 5):ToAllIf(self.Debug) - - -- Get optimal altitiude. - local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) - - -- Get speed hint. - local hintSpeed=self:_SpeedCheck(playerData, speed) - - -- Message to player. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - local hint=string.format("%s\n%s", playerData.step, hintSpeed) - self:MessageToPlayer(playerData, hint, "MARSHAL", "") - end - - -- Next step: Arc Out Turn. - playerData.step=AIRBOSS.PatternStep.ARCOUT - - playerData.warning=nil - end -end - ---- Arc out turn for case II/III recoveries. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_ArcOutTurn(playerData) - - -- Check if player is in valid zone - local validzone=self:_GetZoneCorridor(playerData.case) - - -- Check if we are inside the moving zone. - local invalid=playerData.unit:IsNotInZone(validzone) - - -- Issue warning. - if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") - playerData.warning=true - end - - -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self:_GetZoneArcOut(playerData.case)) - - --if self:_CheckLimits(X, Z, self.DirtyUp) then - if inzone then - - -- Debug message. - MESSAGE:New("Arc Turn Out step reached", 5):ToAllIf(self.Debug) - - -- Get optimal altitiude. - local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) - - -- Get speed hint. - local hintSpeed=self:_SpeedCheck(playerData, speed) - - -- Message to player. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - local hint=string.format("%s\n%s", playerData.step, hintSpeed) - self:MessageToPlayer(playerData, hint, "MARSHAL", "") - end - - -- Next step: - if playerData.case==2 then - -- Case II: Initial. - playerData.step=AIRBOSS.PatternStep.INITIAL - elseif playerdata.case==3 then - -- Case III: Dirty up. - playerData.step=AIRBOSS.PatternStep.DIRTYUP - else - -- ERROR! - end - - playerData.warning=nil - end -end - ---- Dirty up and level out at 1200 ft for case III recovery. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_DirtyUp(playerData) - - -- Check if player is in valid zone - local validzone=self:_GetZoneCorridor(playerData.case) - - -- Check if we are inside the moving zone. - local invalid=playerData.unit:IsNotInZone(validzone) - - -- Issue warning. - if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") - playerData.warning=true - end - - -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) - - --if self:_CheckLimits(X, Z, self.DirtyUp) then - if inzone then - - -- Debug message. - MESSAGE:New("Dirty up step reached", 5):ToAllIf(self.Debug) - - -- Get optimal altitiude. - local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) - - -- Get altitude hint. - local hintAlt, debrief=self:_AltitudeCheck(playerData, altitude) - - -- Get speed hint. - -- TODO: Not sure if we already need to be onspeed AoA at this point? - local hintSpeed=self:_SpeedCheck(playerData, speed) - - -- Message to player. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) - self:MessageToPlayer(playerData, hint, "MARSHAL", "") - end - - -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). - playerData.step=AIRBOSS.PatternStep.BULLSEYE - - playerData.warning=nil - end -end - ---- Intercept glide slop and follow ICLS, aka Bullseye for case III recovery. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Bullseye(playerData) - - -- Check if player is in valid zone - local validzone=self:_GetZoneCorridor(playerData.case) - - -- Check if we are inside the moving zone. - local invalid=playerData.unit:IsNotInZone(validzone) - - -- Issue warning. - if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") - playerData.warning=true - end - - -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) - - -- Check that we reached the position. - --if self:_CheckLimits(X, Z, self.Bullseye) then - if inzone then - - -- Debug message. - MESSAGE:New("Bullseye step reached", 5):ToAllIf(self.Debug) - - -- Get optimal altitiude. - local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) - - -- Get altitude hint. - local hintAlt=self:_AltitudeCheck(playerData, altitude) - - -- Get altitude hint. - local hintAoA=self:_AoACheck(playerData, aoa) - - -- Message to player. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) - self:MessageToPlayer(playerData, hint, "MARSHAL", "") - end - - -- Next step: Groove Call the ball. - playerData.step=AIRBOSS.PatternStep.GROOVE_XX - - playerData.warning=nil - end -end - - ---- Upwind leg or break entry for case I/II recoveries. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Upwind(playerData) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi=self:_GetDistances(playerData.unit) - - -- Abort condition check. - if self:_CheckAbort(X, Z, self.Upwind) then - self:_AbortPattern(playerData, X, Z, self.Upwind, true) - return - end - - -- Check if we are in front of the boat (diffX > 0). - if self:_CheckLimits(X, Z, self.Upwind) then - - -- Get optimal altitude, distance and speed. - local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) - - -- Get altitude hint. - local hintAlt=self:_AltitudeCheck(playerData, alt) - - -- Get speed hint. - local hintSpeed=self:_AltitudeCheck(playerData, speed) - - -- Message to player. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) - self:MessageToPlayer(playerData, hint, "MARSHAL", "") - end - - -- Debrief. - --self:_AddToSummary(playerData, "Entering the Break", debrief) - - -- Next step: Early Break. - playerData.step=AIRBOSS.PatternStep.EARLYBREAK - end -end - - ---- Break. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. --- @param #string part Part of the break. -function AIRBOSS:_Break(playerData, part) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi=self:_GetDistances(playerData.unit) - - -- Early or late break. - local breakpoint = self.BreakEarly - if part=="late" then - breakpoint = self.BreakLate - end - - -- Check abort conditions. - if self:_CheckAbort(X, Z, breakpoint) then - self:_AbortPattern(playerData, X, Z, breakpoint, true) - return - end - - -- Check limits. - if self:_CheckLimits(X, Z, breakpoint) then - - -- Get optimal altitude, distance and speed. - local altitude=self:_GetAircraftParameters(playerData) - - -- Grade altitude. - local hint, debrief=self:_AltitudeCheck(playerData, altitude) - - -- Message to player. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - local hint=string.format("%s %s", playerData.step, hint) - self:MessageToPlayer(playerData, hint, "MARSHAL", "") - end - - -- Debrief - if part=="early" then - self:_AddToSummary(playerData, "Early Break", debrief) - else - self:_AddToSummary(playerData, "Late Break", debrief) - end - - -- Next step: Late Break or Abeam. - if part=="early" then - playerData.step=AIRBOSS.PatternStep.LATEBREAK - else - playerData.step=AIRBOSS.PatternStep.ABEAM - end - end -end - ---- Long downwind leg check. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_CheckForLongDownwind(playerData) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z=self:_GetDistances(playerData.unit) - - -- One NM from carrier is too far. - local limit=UTILS.NMToMeters(-1.5) - - -- Check we are not too far out w.r.t back of the boat. - if X90 and self:_CheckLimits(X, Z, self.Wake) then - -- Message to player. - self:MessageToPlayer(playerData, "You are already at the wake and have not passed the 90! Turn faster next time!", "LSO", "") - --TODO: pattern WO? - end -end - ---- At the Wake. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Wake(playerData) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z = self:_GetDistances(playerData.unit) - - -- Check abort conditions. - if self:_CheckAbort(X, Z, self.Wake) then - self:_AbortPattern(playerData, X, Z, self.Wake, true) - return - end - - -- Right behind the wake of the carrier dZ>0. - if self:_CheckLimits(X, Z, self.Wake) then - - -- Get optimal altitude, distance and speed. - local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) - - -- Grade altitude. - local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) - - -- Grade AoA. - local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) - - -- Message to player. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) - self:MessageToPlayer(playerData, hint, "LSO", "") - end - - -- Debrief. - local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) - - -- Add to debrief. - self:_AddToSummary(playerData, "At the Wake", debrief) - - -- Next step: Final. - playerData.step=AIRBOSS.PatternStep.FINAL - end -end - ---- Turn to final. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Final(playerData) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi = self:_GetDistances(playerData.unit) - - -- In front of carrier or more than 4 km behind carrier. - if self:_CheckAbort(X, Z, self.Groove) then - self:_AbortPattern(playerData, X, Z, self.Groove, true) - return - end - - -- Relative heading 0=fly parallel +-90=fly perpendicular - local relhead=self:_GetRelativeHeading(playerData.unit, true) - - -- Line up wrt runway. - local lineup=self:_Lineup(playerData, true) - - -- Player's angle of bank. - local roll=playerData.unit:GetRoll() - - -- Check if player is in +-5 deg cone and flying towards the runway. - if math.abs(lineup)<5 then --and math.abs(relhead)<5 then - - -- Get optimal altitude, distance and speed. - local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) - - -- Grade altitude. - local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) - - -- AoA feed back - local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) - - -- Message to player. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) - self:MessageToPlayer(playerData, hint, "LSO", "") - end - - -- Add to debrief. - local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) - self:_AddToSummary(playerData, "Enter Groove", debrief) - - -- Gather pilot data. - local groovedata={} --#AIRBOSS.GrooveData - groovedata.Step=playerData.step - groovedata.Alt=alt - groovedata.AoA=aoa - groovedata.GSE=self:_Glideslope(playerData, 3.5) - groovedata.LUE=self:_Lineup(playerData, true) - groovedata.Roll=roll - groovedata.Rhdg=relhead - - -- TODO: could add angled approach if lineup<5 and relhead>5. This would mean the player has not tunred in correctly! - - -- Groove - playerData.groove.X0=groovedata - - -- Next step: X start & call the ball. - playerData.step=AIRBOSS.PatternStep.GROOVE_XX - end - -end - - ---- In the groove. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Groove(playerData) - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi = self:_GetDistances(playerData.unit) - - -- Player altitude - local alt=playerData.unit:GetAltitude() - - -- Player group. - local player=playerData.unit:GetGroup() - - -- Check abort conditions. - if self:_CheckAbort(X, Z, self.Trap) then - self:_AbortPattern(playerData, X, Z, self.Trap, true) - return - end - - -- Lineup with runway centerline. - local lineupError=self:_Lineup(playerData, true) - - -- Glide slope. - local glideslopeError=self:_Glideslope(playerData, 3.5) - - -- Get AoA. - local AoA=playerData.unit:GetAoA() - - -- Ranges in the groove. - local RXX=UTILS.NMToMeters(0.750)+math.abs(self.carrierparam.sterndist) -- Start of groove. 0.75 = 1389 m - local RRB=UTILS.NMToMeters(0.500)+math.abs(self.carrierparam.sterndist) -- Roger Ball! call. 0.5 = 926 m - local RIM=UTILS.NMToMeters(0.375)+math.abs(self.carrierparam.sterndist) -- In the Middle 0.75/2. 0.375 = 695 m - local RIC=UTILS.NMToMeters(0.100)+math.abs(self.carrierparam.sterndist) -- In Close. 0.1 = 185 m - local RAR=UTILS.NMToMeters(0.000)+math.abs(self.carrierparam.sterndist) -- At the Ramp. - - -- Data - local groovedata={} --#AIRBOSS.GrooveData - groovedata.Step=playerData.step - groovedata.Alt=alt - groovedata.AoA=AoA - groovedata.GSE=glideslopeError - groovedata.LUE=lineupError - groovedata.Roll=playerData.unit:GetRoll() - groovedata.Rhdg=self:_GetRelativeHeading(playerData.unit, true) - - if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX then - - -- LSO "Call the ball" call. - self:RadioTransmission(self.LSOradio, self.radiocall.CALLTHEBALL) - playerData.Tlso=timer.getTime() - - -- Store data. - playerData.groove.XX=groovedata - - -- Next step: roger ball. - playerData.step=AIRBOSS.PatternStep.GROOVE_RB - - elseif rho<=RRB and playerData.step==AIRBOSS.PatternStep.GROOVE_RB then - - -- Pilot: "Roger ball" call. - self:RadioTransmission(self.LSOradio, self.radiocall.ROGERBALL) - playerData.Tlso=timer.getTime()+1 - - -- Store data. - playerData.groove.RB=groovedata - - -- Next step: in the middle. - playerData.step=AIRBOSS.PatternStep.GROOVE_IM - - elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then - - -- Debug. - local text=string.format("FF IM=%d", rho) - MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:I(self.lid..string.format("FF IM=%d", rho)) - - -- Store data. - playerData.groove.IM=groovedata - - -- Next step: in close. - playerData.step=AIRBOSS.PatternStep.GROOVE_IC - - elseif rho<=RIC and playerData.step==AIRBOSS.PatternStep.GROOVE_IC then - - -- Check if player was already waved off. - if playerData.waveoff==false then - - -- Debug - local text=string.format("FF IC=%d", rho) - MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:I(self.lid..text) - - -- Store data. - playerData.groove.IC=groovedata - - -- Check if player should wave off. - local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA) - - -- Let's see.. - if waveoff then - - -- LSO Wave off! - self:RadioTransmission(self.LSOradio, self.radiocall.WAVEOFF) - playerData.Tlso=timer.getTime() - - -- Player was waved off! - playerData.waveoff=true - - return - else - -- Next step: AR at the ramp. - playerData.step=AIRBOSS.PatternStep.GROOVE_AR - end - - end - - elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AR then - - -- Debug. - local text=string.format("FF AR=%d", rho) - MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:I(self.lid..text) - - -- Store data. - playerData.groove.AR=groovedata - - -- Next step: in the wires. - playerData.step=AIRBOSS.PatternStep.GROOVE_IW - end - - -- Time since last LSO call. - local time=timer.getTime() - local deltaT=time-playerData.Tlso - - -- Check if we are beween 3/4 NM and end of ship. - if rho>=RAR and rho=3 and playerData.waveoff==false then - - -- LSO call if necessary. - self:_LSOadvice(playerData, glideslopeError, lineupError) - - elseif X>100 then - - if playerData.landed then - - -- Add to debrief. - if playerData.waveoff then - self:_AddToSummary(playerData, "Wave Off", "You were waved off but landed anyway. Airboss wants to talk to you!") - else - self:_AddToSummary(playerData, "Bolter", "You boltered.") - end - - else - - -- Add to debrief. - self:_AddToSummary(playerData, "Wave Off", "You were waved off.") - - -- Next step: debrief. - playerData.step=AIRBOSS.PatternStep.DEBRIEF - - end - end -end - ---- LSO check if player needs to wave off. --- Wave off conditions are: --- --- * Glide slope error > 3 degrees. --- * Line up error > 3 degrees. --- * AoA<6.9 or AoA>9.3. --- @param #AIRBOSS self --- @param #number glideslopeError Glide slope error in degrees. --- @param #number lineupError Line up error in degrees. --- @param #number AoA Angle of attack of player aircraft. --- @return #boolean If true, player should wave off! -function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA) - - local waveoff=false - - -- Too high or too low? - if math.abs(glideslopeError)>1 then - self:I(self.lid.."Wave off due to glide slope error >1 degrees!") - waveoff=true - end - - -- Too far from centerline? - if math.abs(lineupError)>3 then - self:I(self.lid.."Wave off due to line up error >3 degrees!") - waveoff=true - end - - -- Too slow or too fast? - -- TODO: Only apply for TOPGUN graduate skill level or at least not for Flight Student level. - if AoA<6.9 or AoA>9.3 then - self:I(self.lid.."INACTIVE! Wave off due to AoA<6.9 or AoA>9.3!") - --waveoff=true - end - - return waveoff -end - ---- Get wire from landing position. --- @param #AIRBOSS self --- @param #number d Distance in meters wrt carrier position where player landed. --- @param #number dx Correction. -function AIRBOSS:_GetWire(d, dx) - - -- Little offset for the exact wire positions. - dx=dx or 30 - - -- Which wire was caught? X>0 since calculated as distance! - local wire - if d-dx wire=%d.", d, dx, d-dx, wire)) - - return wire -end - ---- Trapped? --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. --- @param #number wire The wire caught. -function AIRBOSS:_Trapped(playerData, wire) - - if playerData.unit:InAir()==false then - -- Seems we have successfully landed. - - -- Message to player. - local text=string.format("Trapped! %d-wire.", wire) - self:MessageToPlayer(playerData, text, "LSO", "") - - -- Debrief. - local hint = string.format("Trapped catching the %d-wire.", wire) - self:_AddToSummary(playerData, "Goove: IW", hint) - - else - --Still in air ==> Boltered! - playerData.boltered=true - end - - -- Next step: debriefing. - playerData.step=AIRBOSS.PatternStep.DEBRIEF -end - ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ORIENTATION functions ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4018,7 +4002,12 @@ function AIRBOSS:_Glideslope(playerData, gangle) -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. local h=playerData.unit:GetAltitude()-self.carrierparam.deckheight - local x=math.abs(self.carrierparam.wire3-X) --TODO: Check if carrier has wires later. + + -- Distance correction. + local offx=self.carrierparam.wire3 or self.carrierparam.sterndist + local x=math.abs(self.carrierparam.wire3-X) + + -- Glide slope. local glideslope=math.atan(h/x) return math.deg(glideslope)-gangle @@ -4704,7 +4693,7 @@ function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) text=text.." Depart and re-enter!" -- Add to debrief. - self:_AddToSummary(playerData, string.format("%s", playerData.step), string.format("Pattern wave off: %s", text)) + self:_AddToDebrief(playerData, string.format("Pattern wave off: %s", text)) -- Next step debrief. playerData.step=AIRBOSS.PatternStep.DEBRIEF @@ -4936,20 +4925,21 @@ function AIRBOSS:_SpeedCheck(playerData, speedopt) return hint, debrief end ---- Append text to debrief text. +--- Append text to debriefing. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. --- @param #string step Current step in the pattern. --- @param #string item Text item appeded to the debrief. -function AIRBOSS:_AddToSummary(playerData, step, item) - table.insert(playerData.debrief, {step=step, hint=item}) +-- @param #string hint Debrief text of this step. +-- @param #string step (Optional) Current step in the pattern. Default from playerData. +function AIRBOSS:_AddToDebrief(playerData, hint, step) + step=step or playerData.step + table.insert(playerData.debrief, {step=step, hint=hint}) end ---- Show debriefing message. +--- Debrief player and set next step. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. function AIRBOSS:_Debrief(playerData) - self:F("Debriefing") + self:F2(self.lid..string.format("Debriefing of player %s.", playerData.name)) -- LSO grade, points, and flight data analyis. local grade, points, analysis=self:_LSOgrade(playerData) @@ -4966,28 +4956,53 @@ function AIRBOSS:_Debrief(playerData) -- LSO grade message. local text=string.format("%s %.1f PT - %s\n", grade, points, analysis) text=text..string.format("Your detailed debriefing can be found via the F10 radio menu.") - self:MessageToPlayer(playerData,text, "LSO", "", 30, true) + self:MessageToPlayer(playerData, text, "LSO", "", 30, true) -- Check if boltered or waved off? if playerData.boltered or playerData.waveoff or playerData.patternwo then - -- TODO: Can become nil when I crashed and changed to observer. Which events are captured? Nil check for unit? - - -- Get heading and distance to register zone ~3 NM astern. - local heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) - local distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) - - -- Re-enter message. - local text=string.format("fly heading %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) - self:MessageToPlayer(playerData, text, "LSO", nil, 10) - -- Next step? -- TODO: CASE I: After bolter/wo turn left and climb to 600 ft and re-enter the pattern. But do not go to initial but reenter earlier? -- TODO: CASE I: After pattern wo? go back to initial, I guess? -- TODO: CASE III: After bolter/wo turn left and climb to 1200 ft and re-enter pattern? - -- TODO: CASE III: After pattern wo? No idea... + -- TODO: CASE III: After pattern wo? No idea... + + -- Can become nil when I crashed and changed to observer. Which events are captured? Nil check for unit? + + if playerData.unit:IsAlive() then - playerData.step=AIRBOSS.PatternStep.COMMENCING + -- Heading and distance tip. + local heading, distance + + if playerData.case==1 or playerData.case==2 then + + -- Get heading and distance to initial zone ~3 NM astern. + heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) + distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) + + elseif playerData.case==3 then + + -- Get heading and distance to bullseye zone ~3 NM astern. + local zone=self:_GetZoneBullseye(playerData.case) + + heading=playerData.unit:GetCoordinate():HeadingTo(zone:GetCoordinate()) + distance=playerData.unit:GetCoordinate():Get2DDistance(zone:GetCoordinate()) + + end + + -- Re-enter message. + local text=string.format("fly heading %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) + self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 5) + + + -- Commencing again. + playerData.step=AIRBOSS.PatternStep.COMMENCING + + else + -- Unit does not seem to be alive! + -- TODO: What now? + self:I(self.lid..string.format("Player unit not alive!")) + end elseif playerData.landed and not playerData.unit:InAir() then @@ -4995,7 +5010,7 @@ function AIRBOSS:_Debrief(playerData) self:_RemoveUnitFromFlight(playerData.unit) -- Message to player. - self:MessageToPlayer(playerData, "Welcome on board!", "LSO", nil, 10) + self:MessageToPlayer(playerData, string.format("Welcome on board, %s!", playerData.name), "AIRBOSS", "", 10) else @@ -5003,17 +5018,72 @@ function AIRBOSS:_Debrief(playerData) self:MessageToPlayer(playerData, "Undefined state after landing! Please report.", "ERROR", nil, 10) -- Next step. - playerData.step=AIRBOSS.PatternStep.UNDEFINED + playerData.step=AIRBOSS.PatternStep.UNDEFINED end - MESSAGE:New(string.format("Player step %s.", playerData.step)) + -- Increase number of passes. + playerData.passes=playerData.passes+1 + -- Debug message. + MESSAGE:New(string.format("Player step %s.", playerData.step), 5, "DEBUG"):ToAllIf(self.Debug) end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- MISC functions ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get onboard number of player or client. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #string Onboard number as string. +function AIRBOSS:_GetOnboardNumberPlayer(group) + return self:_GetOnboardNumbers(group, true) +end + +--- Get onboard numbers of all units in a group. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @param #boolean playeronly If true, return the onboard number for player or client skill units. +-- @return #table Table of onboard numbers. +function AIRBOSS:_GetOnboardNumbers(group, playeronly) + --self:F({groupname=group:GetName}) + + -- Get group name. + local groupname=group:GetName() + + -- Debug text. + local text=string.format("Onboard numbers of group %s:", groupname) + + -- Units of template group. + local units=group:GetTemplate().units + + -- Get numbers. + local numbers={} + for _,unit in pairs(units) do + + -- Onboard number and unit name. + local n=tostring(unit.onboard_num) + local name=unit.name + local skill=unit.skill + + -- Debug text. + text=text..string.format("\n- unit %s: onboard #=%s skill=%s", name, n, skill) + + if playeronly and skill=="Client" or skill=="Player" then + -- There can be only one player in the group, so we skip everything else. + return n + end + + -- Table entry. + numbers[name]=n + end + + -- Debug info. + self:I(self.lid..text) + + return numbers +end + --- Check if aircraft is capable of landing on an aircraft carrier. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.) @@ -5765,11 +5835,12 @@ function AIRBOSS:_SetSection(_unitName) local flight=_flight --#AIRBOSS.PlayerData text=text..string.format("- %s", flight.name) flight.seclead=playerData.name + -- Inform player that he is now part of a section. - self:MessageToPlayer(flight, string.format("Your section lead is not %s", playerData.name), self.carrier:GetName(), "", 10) + self:MessageToPlayer(flight, string.format("Your section lead is now %s", playerData.name), self.carrier:GetName(), "", 10) end else - text="No other human flights found within radius of 200 meter radius!" + text="No other human flights found within radius of 200 meters!" end -- Message to section lead. @@ -6181,7 +6252,7 @@ function AIRBOSS:_MarkMarshalZone(_unitName, flare) if playerData then -- Get current holding zone. - local zone=self:_GetHoldingZone(playerData) + local zone=self:_GetZoneHolding(playerData.case, playerData.flag:Get()) local text="No marshal zone to smoke!" if zone then From b05d3fbde21c93cff956fcde42205517977cfd8e Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 30 Nov 2018 23:48:59 +0100 Subject: [PATCH 070/147] AIRBOSS v0.4.1 --- Moose Development/Moose/Ops/Airboss.lua | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index e8fb6e0f3..38a8b109a 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -465,7 +465,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.0w" +AIRBOSS.version="0.4.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -564,13 +564,13 @@ function AIRBOSS:New(carriername, alias) self:SetTACAN() -- Set max aircraft in landing pattern. - self:SetMaxLandingPattern(1) + self:SetMaxLandingPattern(2) -- Set holding offset to 0 degrees. self:SetHoldingOffsetAngle(30) -- Default recovery case. - self:SetRecoveryCase(3) + self:SetRecoveryCase(1) -- CCA 50 NM radius zone around the carrier. self:SetCarrierControlledArea() @@ -1160,13 +1160,13 @@ function AIRBOSS:_InitStennis() -- Upwind leg (break entry). self.Upwind.name="Upwind" - self.Upwind.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of ?. + self.Upwind.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. self.Upwind.Xmax= nil - self.Upwind.Zmin=-100 -- Not more than 100 meters port of boat. - self.Upwind.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard of boat. + self.Upwind.Zmin=-400 -- Not more than 400 meters port of boat. Otherwise miss the zone. + self.Upwind.Zmax= 600 -- Not more than 600 m starboard of boat. Otherwise miss the zone. self.Upwind.LimitXmin=0 -- Check and next step when at carrier and starboard of carrier. self.Upwind.LimitXmax=nil - self.Upwind.LimitZmin=0 + self.Upwind.LimitZmin=-100 self.Upwind.LimitZmax=nil -- Early break. @@ -2357,8 +2357,8 @@ function AIRBOSS:_CheckPlayerStatus() elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then - -- Debriefing in 5 seconds. - SCHEDULER:New(nil, self._Debrief, {self, playerData}, 5) + -- Debriefing in 10 seconds. + SCHEDULER:New(nil, self._Debrief, {self, playerData}, 10) -- Undefined status. playerData.step=AIRBOSS.PatternStep.UNDEFINED @@ -4672,8 +4672,8 @@ end -- @param #AIRBOSS.PlayerData playerData Player data. -- @param #number X X distance player to carrier. -- @param #number Z Z distance player to carrier. --- @param #boolean patternwo (Optional) Pattern wave off. -- @param #AIRBOSS.Checkpoint posData Checkpoint data. +-- @param #boolean patternwo (Optional) Pattern wave off. function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) -- Text where we are wrong. @@ -4791,7 +4791,7 @@ function AIRBOSS:_DistanceCheck(playerData, optdist) end -- Distance to carrier. - local distance=playerData.unit:GetCoodinate():Get2DDistance(self:GetCoordinate()) + local distance=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) -- Get relative score. local lowscore, badscore = self:_GetGoodBadScore(playerData) From cf2ad6f2771ccd6c6d64e39b25d6037fe4e8da6d Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 2 Dec 2018 00:30:22 +0100 Subject: [PATCH 071/147] AIRBOSS v0.4.2 --- Moose Development/Moose/Ops/Airboss.lua | 769 ++++++++++++------ .../Moose/Ops/RecoveryTanker.lua | 3 +- 2 files changed, 509 insertions(+), 263 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 38a8b109a..87cc8a3bb 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -8,16 +8,19 @@ -- * Supports human pilots as well as AI flight groups. -- * Automatic LSO grading. -- * Different skill levels from on-the-fly tips for flight students to ziplip for pros. --- * Recovery tanker option. --- * Voice overs for LSO and AIRBOSS calls. --- * Automatic TACAN and ICLS channel setting. --- * Different radio channels for LSO and airboss calls. --- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels, LSO grades). +-- * Define recovery time windows with individual recovery cases. +-- * Automatic TACAN and ICLS channel setting of carrier. +-- * Separate radio channels for LSO and Marshal/Airboss transmissions. +-- * Voice over support for LSO, Marshal and Airboss radio transmissions. +-- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, help function (player aircraft attitude, marking of pattern zones etc). +-- * Recovery tanker and refueling option via integration of @{#Ops.RecoveryTanker} class. +-- * Rescue helo option via @{#Ops.RescueHelo} class. -- * Multiple carriers supported (due to object oriented approach). -- -- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much work in progress. -- --- At the moment training parameters are optimized for F/A-18C Hornet as aircraft and USS John C. Stennis as carrier. +-- At the moment, parameters are optimized for F/A-18C Hornet as aircraft and USS John C. Stennis as carrier. +-- The community mod A-4E is also supported in priciple but needs further tweaking of parameters suche as on speed AoA values. -- Other aircraft and carriers **might** be possible in future but would need a different set of optimized parameters. -- -- === @@ -71,7 +74,7 @@ -- @field #number Nmaxpattern Max number of aircraft in landing pattern. -- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. -- @field Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. --- @field #table recoverytime List of time intervals when aircraft are recovered. +-- @field #table recoverytimes List of time windows when aircraft are recovered including the recovery case. -- @field #number holdingoffset Offset [degrees] of Case II/III holding pattern. Default 0 degrees. -- @extends Core.Fsm#FSM @@ -88,9 +91,10 @@ -- # Recovery Cases -- -- The AIRBOSS class supports all three commonly used recovery cases, i.e. --- * CASE I, which is for daytime and good weather --- * CASE II, for daytime but poor visibility conditions and --- * CASE III for nighttime recoveries. +-- +-- * CASE I: For daytime and good weather, +-- * CASE II: For daytime but poor visibility conditions, +-- * CASE III: For nighttime recoveries. -- -- ## CASE I -- @@ -112,51 +116,51 @@ -- -- @field #AIRBOSS AIRBOSS = { - ClassName = "AIRBOSS", - lid = nil, - Debug = true, - carrier = nil, - carriertype = nil, - carrierparam = {}, - alias = nil, - airbase = nil, - beacon = nil, - TACANchannel = nil, - TACANmode = nil, - ICLSchannel = nil, - LSOradio = nil, - LSOfreq = nil, - Carrierradio = nil, - Carrierfreq = nil, - radiocall = {}, - radiotimer = nil, - zoneCCA = nil, - zoneCCZ = nil, - zoneInitial = nil, - players = {}, - menuadded = {}, - Upwind = {}, - Abeam = {}, - BreakEarly = {}, - BreakLate = {}, - Ninety = {}, - Wake = {}, - Groove = {}, - Trap = {}, - Platform = {}, - DirtyUp = {}, - Bullseye = {}, - case = 1, - flights = {}, - Qpattern = {}, - Qmarshal = {}, - RQMarshal = {}, - RQLSO = {}, - Nmaxpattern = nil, - tanker = nil, - warehouse = nil, - recoverytime = {}, - holdingoffset= nil, + ClassName = "AIRBOSS", + lid = nil, + Debug = true, + carrier = nil, + carriertype = nil, + carrierparam = {}, + alias = nil, + airbase = nil, + beacon = nil, + TACANchannel = nil, + TACANmode = nil, + ICLSchannel = nil, + LSOradio = nil, + LSOfreq = nil, + Carrierradio = nil, + Carrierfreq = nil, + radiocall = {}, + radiotimer = nil, + zoneCCA = nil, + zoneCCZ = nil, + zoneInitial = nil, + players = {}, + menuadded = {}, + Upwind = {}, + Abeam = {}, + BreakEarly = {}, + BreakLate = {}, + Ninety = {}, + Wake = {}, + Groove = {}, + Trap = {}, + Platform = {}, + DirtyUp = {}, + Bullseye = {}, + case = 1, + flights = {}, + Qpattern = {}, + Qmarshal = {}, + RQMarshal = {}, + RQLSO = {}, + Nmaxpattern = nil, + tanker = nil, + warehouse = nil, + recoverytimes = {}, + holdingoffset = nil, } --- Player aircraft types capable of landing on carriers. @@ -204,7 +208,7 @@ AIRBOSS.CarrierType={ KUZNETSOV="KUZNECOW", } ---- Carrier Parameters. +--- Carrier specific parameters. -- @type AIRBOSS.CarrierParameters -- @field #number rwyangle Runway angle in degrees. for carriers with angled deck. For USS Stennis -9 degrees. -- @field #number sterndist Distance in meters from carrier position to stern of carrier. For USS Stennis -150 meters. @@ -214,10 +218,13 @@ AIRBOSS.CarrierType={ -- @field #number wire3 Distance in meters from carrier position to third wire. -- @field #number wire4 Distance in meters from carrier position to fourth wire. ---- Aircraft Parameters. --- @type AIRBOSS.AircraftParameters --- @field #number AoA Onspeed Angle of Attack. --- @field #number Dboat Ideal distance to the carrier. +--- Aircraft specific Angle of Attack (AoA) (or alpha) parameters. +-- @type AIRBOSS.AircraftAoA +-- @field #number OnSpeedMin Minimum on speed AoA. Values below are fast +-- @field #number OnSpeedMax Maximum on speed AoA. Values above are slow. +-- @field #number OnSpeed Optimal AoA. +-- @field #number Fast Fast AoA threshold. Smaller means faster. +-- @field #number Slow Slow AoA threshold. Larger means slower --- Pattern steps. -- @type AIRBOSS.PatternStep @@ -361,6 +368,7 @@ AIRBOSS.Difficulty={ -- @type AIRBOSS.Recovery -- @field #number START Start of recovery. -- @field #number STOP End of recovery. +-- @field #number CASE Recovery case (1-3) of that time slot. --- Groove position. -- @type AIRBOSS.GroovePos @@ -465,22 +473,25 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.1" +AIRBOSS.version="0.4.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Extract (static) weather from mission for cloud covery etc. +-- TODO: Option to filter AI groups for recovery. +-- TODO: Option to turn AI handling off. -- TODO: Check distance to players during approach. PWO if too close. --- TODO: Spin pattern. Add radio menu entry. --- TODO: Set case II and III times. +-- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! -- TODO: Add radio check (LSO, AIRBOSS) to F10 radio menu. -- TODO: Add user functions. -- TODO: Generalize parameters for other carriers. -- TODO: Generalize parameters for other aircraft. -- TODO: Foul deck check. -- TODO: Persistence of results. --- TODO: Right pattern step after bolter/wo/patternWO? +-- DONE: Right pattern step after bolter/wo/patternWO? Guess so. +-- DONE: Set case II and III times (via recovery time). -- DONE: Get correct wire when trapped. DONE but might need further tweaking. -- DONE: Add radio transmission queue for LSO and airboss. -- TONE: CASE II. @@ -560,14 +571,14 @@ function AIRBOSS:New(carriername, alias) -- Set ICSL to channel 1. self:SetICLS() - -- Set TACAN to channel 74X + -- Set TACAN to channel 74X. self:SetTACAN() -- Set max aircraft in landing pattern. self:SetMaxLandingPattern(2) -- Set holding offset to 0 degrees. - self:SetHoldingOffsetAngle(30) + self:SetHoldingOffsetAngle(45) -- Default recovery case. self:SetRecoveryCase(1) @@ -597,8 +608,7 @@ function AIRBOSS:New(carriername, alias) -- CASE I/II moving zone: Zone 3 NM astern and 100 m starboard of the carrier with radius of 0.5 km. self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, 0.5*1000, {dx=-UTILS.NMToMeters(3), dy=100, relative_to_unit=true}) - - + -- Smoke zones. if self.Debug and false then local case=2 @@ -631,13 +641,14 @@ function AIRBOSS:New(carriername, alias) self:SetStartState("Stopped") -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("Stopped", "Start", "Idle") -- Start AIRBOSS script. - self:AddTransition("*", "Idle", "Idle") -- Carrier is idleing. - self:AddTransition("Idle", "Recover", "Recovering") -- Recover aircraft. - self:AddTransition("*", "Status", "*") -- Update status of players and queues. - self:AddTransition("*", "Case", "*") -- Switch to another case recovery. - self:AddTransition("*", "Stop", "Stopped") -- Stop AIRBOSS script. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Idle") -- Start AIRBOSS script. + self:AddTransition("*", "Idle", "Idle") -- Carrier is idling. + self:AddTransition("Idle", "RecoveryStart", "Recovering") -- Start recovering aircraft. + self:AddTransition("Recovering", "RecoveryStop", "Idle") -- Stop recovering aircraft. + self:AddTransition("*", "Status", "*") -- Update status of players and queues. + self:AddTransition("*", "RecoveryCase", "*") -- Switch to another case recovery. + self:AddTransition("*", "Stop", "Stopped") -- Stop AIRBOSS FMS. --- Triggers the FSM event "Start" that starts the airboss. Initializes parameters and starts event handlers. @@ -650,38 +661,48 @@ function AIRBOSS:New(carriername, alias) -- @param #number delay Delay in seconds. - --- Triggers the FSM event "Idle" so no operations are carried out. + --- Triggers the FSM event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out. -- @function [parent=#AIRBOSS] Idle -- @param #AIRBOSS self - --- Triggers the FSM event "Idle" after a delay. + --- Triggers the FSM delayed event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out. -- @function [parent=#AIRBOSS] __Idle -- @param #AIRBOSS self -- @param #number delay Delay in seconds. - --- Triggers the FSM event "Recover" that starts the recovering of aircraft. Marshalling aircraft are send to the landing pattern. - -- @function [parent=#AIRBOSS] Recover + --- Triggers the FSM event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern. + -- @function [parent=#AIRBOSS] RecoveryStart + -- @param #AIRBOSS self + -- @param #number Case Recovery case (1, 2 or 3) that is started. + + --- Triggers the FSM delayed event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern. + -- @function [parent=#AIRBOSS] __RecoveryStart + -- @param #AIRBOSS self + -- @param #number Case Recovery case (1, 2 or 3) that is started. + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RecoveryStop" that stops the recovery of aircraft. + -- @function [parent=#AIRBOSS] RecoveryStop -- @param #AIRBOSS self - --- Triggers the FSM event "Recover" that starts the recovering of aircraft after a delay. Marshalling aircraft are send to the landing pattern. - -- @function [parent=#AIRBOSS] __Recover + --- Triggers the FSM delayed event "RecoveryStop" that stops the recovery of aircraft. + -- @function [parent=#AIRBOSS] __RecoveryStop -- @param #AIRBOSS self -- @param #number delay Delay in seconds. - --- Triggers the FSM event "Case" that switches the recovery case. - -- @function [parent=#AIRBOSS] Case + --- Triggers the FSM event "RecoveryCase" that switches the aircraft recovery case. + -- @function [parent=#AIRBOSS] RecoveryCase -- @param #AIRBOSS self - -- @param #number OldCase Old recovery case. - -- @param #number NewCase New recovery case. + -- @param #number Case The new recovery case (1, 2 or 3). - --- Triggers the delayed FSM event "Case" that switches the recovery case + --- Triggers the delayed FSM event "RecoveryCase" that sets the used aircraft recovery case. -- @function [parent=#AIRBOSS] __Case -- @param #AIRBOSS self -- @param #number delay Delay in seconds. - -- @param #number OldCase Old recovery case. - -- @param #number NewCase New recovery case. + -- @param #number Case The new recovery case (1, 2 or 3). --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. @@ -751,21 +772,38 @@ function AIRBOSS:SetHoldingOffsetAngle(offset) return self end ---- Add recovery time slot. +--- Add aircraft recovery time window and recovery case. -- @param #AIRBOSS self --- @param #string starttime Start time, e.g. "8:00" for eight o'clock. --- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. +-- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now. +-- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. +-- @param #number case Recovery case for that time slot. Number between one and three. Default 1. -- @return #AIRBOSS self -function AIRBOSS:AddRecoveryTime(starttime, stoptime) +function AIRBOSS:AddRecoveryTime(starttime, stoptime, case) - local Tstart=UTILS.ClockToSeconds(starttime) - local Tstop=UTILS.ClockToSeconds(stoptime) + -- Set start time. + local Tstart=UTILS.ClockToSeconds(starttime or UTILS.SecondsToClock(timer.getAbsTime())) + -- Set stop time. + local Tstop=UTILS.ClockToSeconds(stoptime or Tstart+90*60) + + -- Consistancy check for timing. + if Tstart>Tstop then + self:E(string.format("ERROR: Recovery stop time %s lies before recovery start time %s! Recovery windows rejected.", UTILS.SecondsToClock(Tstart), UTILS.SecondsToClock(Tstop))) + return self + end + + -- Default is Case 1 recovery. + case=case or 1 + + -- Recovery window. local rtime={} --#AIRBOSS.Recovery rtime.START=Tstart rtime.STOP=Tstop + rtime.CASE=case + + -- Add to table + table.insert(self.recoverytimes, rtime) - table.insert(self.recoverytime, rtime) return self end @@ -774,8 +812,9 @@ end -- @param #AIRBOSS self -- @param #number channel TACAN channel. Default 74. -- @param #string mode TACAN mode, i.e. "X" or "Y". Default "X". +-- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". -- @return #AIRBOSS self -function AIRBOSS:SetTACAN(channel, mode) +function AIRBOSS:SetTACAN(channel, mode, moresecode) self.TACANchannel=channel or 74 self.TACANmode=mode or "X" @@ -935,20 +974,16 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Get current time. local time=timer.getTime() - - -- Check if we go into recovery mode. - local recovery=self:_CheckRecoveryTimes() - if recovery==true then - self:Recover() - elseif recovery==false then - self:Idle() - end - + -- Update marshal and pattern queue every 30 seconds. if time-self.Tqueue>30 then + -- Debug info. local text=string.format("Status %s.", self:GetState()) self:I(self.lid..text) + + -- Check recovery times and start/stop recovery mode if necessary. + self:_CheckRecoveryTimes() -- Scan carrier zone for new aircraft. self:_ScanCarrierZone() @@ -968,14 +1003,79 @@ function AIRBOSS:onafterStatus(From, Event, To) self:__Status(-0.5) end ---- Check if recovery times. +--- Check recovery times and start/stop recovery mode of aircraft. -- @param #AIRBOSS self --- @return #boolean IF true, start recovery. function AIRBOSS:_CheckRecoveryTimes() -- Get current abs time. - local abstime=timer.getAbsTime() + local time=timer.getAbsTime() + local Cnow=UTILS.SecondsToClock(time) + -- Debug output: + local text=string.format(self.lid.."Recovery time windows:") + + -- Handle case with no recoveries. + if #self.recoverytimes==0 then + text=" none!" + end + + -- Loop over all slots. + for _,_recovery in pairs(self.recoverytimes) do + local recovery=_recovery --#AIRBOSS.Recovery + + -- Get start/stop clock strings. + local Cstart=UTILS.SecondsToClock(recovery.START) + local Cstop=UTILS.SecondsToClock(recovery.STOP) + + -- Status info. + local state="" + + -- Check if start time passed. + if time>=recovery.START then + -- Start time has passed. + + if time=rtime.START and abstime<=rtime.STOP then -- This is a valid time slot. Do not touch recovery again! - recovery=true + recovery=rtime elseif abstime>rtime.STOP then -- Stop time has already passed. - table.insert(remove, i) + -- We do not remove the time sind the above #recovery check would fail by automatically setting case 1 always! + --table.insert(remove, i) elseif abstime no switch necessary - return false - end + --]] - if NewCase<1 or NewCase>3 then - self:E(self.lid.."ERROR: new case is not 1, 2 or 3 but %s", tostring(NewCase)) - return false - end +end + + +--- On after "RecoveryCase" event. Sets new aircraft recovery case. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Case The recovery case (1, 2 or 3) to switch to. +function AIRBOSS:onafterRecoveryCase(From, Event, To, Case) + + -- Debug output. + self:I(self.lid..string.format("Switching to recovery case %d.", Case)) - return true + -- Set new recovery case. + self.case=Case end ---- On after "Case" event. +--- On after "RecoveryStart" event. Recovery of aircraft is started and carrier switches to state "Recovering". -- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #number OldCase The old (current) case. --- @param #number NewCase The new case. -function AIRBOSS:onbeforeCase(From, Event, To, OldCase, NewCase) +-- @param #number Case The recovery case (1, 2 or 3) to start. +function AIRBOSS:onafterRecoveryStart(From, Event, To, Case) - self.case=NewCase + -- Debug output. + self:I(self.lid..string.format("Starting aircraft recovery in case %d.", Case)) + + -- Switch to case. + self:RecoveryCase(Case) + end - ---- On before "Recover" event. +--- On after "RecoveryStop" event. Recovery of aircraft is stopped and carrier switches to state "Idle". -- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @return #boolean If true, recovery transition is allowed. -function AIRBOSS:onbeforeRecover(From, Event, To) - return true +function AIRBOSS:onafterRecoveryStop(From, Event, To) + + -- Debug output. + self:I(self.lid..string.format("Stopping aircraft recovery.")) + + -- Switch to idle state. + self:Idle() + +end + + +--- On after "Idle" event. Carrier goes to state "Idle". +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterIdle(From, Event, To) + -- Debug output. + self:I(self.lid..string.format("Carrier goes to idle.")) end @@ -1112,16 +1229,7 @@ function AIRBOSS:_InitStennis() self.carrierparam.wire2 = -92 self.carrierparam.wire3 = -80 self.carrierparam.wire4 = -68 - - --[[ - q0=self.carrier:GetCoordinate():SetAltitude(25) - q0:BigSmokeSmall(0.1) - q1=self.carrier:GetCoordinate():Translate(-104,0):SetAltitude(22) --1st wire - q1:BigSmokeSmall(0.1)--:SmokeGreen() - q2=self.carrier:GetCoordinate():Translate(-68,0):SetAltitude(22) --4th wire ==> distance between wires 12 m - q2:BigSmokeSmall(0.1)--:SmokeBlue() - ]] - + -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight. self.Platform.name="Platform 5k" self.Platform.Xmin=-UTILS.NMToMeters(22) -- Not more than 22 NM behind the boat. Last check was at 21 NM. @@ -1224,6 +1332,7 @@ function AIRBOSS:_InitStennis() self.Wake.LimitZmin=0 -- Check and next step when directly behind the boat. self.Wake.LimitZmax=nil + -- TODO: rename to final -- Turn to final. self.Groove.name="Groove" self.Groove.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. @@ -1235,6 +1344,7 @@ function AIRBOSS:_InitStennis() self.Groove.LimitZmin=nil self.Groove.LimitZmax=nil + -- TODO rename to groove -- In the Groove. self.Trap.name="Trap" self.Trap.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. @@ -1248,6 +1358,44 @@ function AIRBOSS:_InitStennis() end +--- Get optimal aircraft AoA parameters.. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #AIRBOSS.AircraftAoA AoA parameters for the given aircraft type. +function AIRBOSS:_GetAircraftAoA(playerData) + + -- Get AC type. + local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET + local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC + local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B + + local aoa -- #AIRBOSS.AircraftAoA + + if hornet then + aoa.Slow=9.3 + aoa.OnSpeedMax=8.8 + aoa.OnSpeed=8.1 + aoa.OnSpeedMin=7.4 + aoa.Fast=6.9 + elseif skyhawk then + -- TODO: A-4 parameters! On speed AoA? + aoa.Slow=9.3 + aoa.OnSpeedMax=8.8 + aoa.OnSpeed=8.1 + aoa.OnSpeedMin=7.4 + aoa.Fast=6.9 + elseif harrier then + -- TODO: AV-8B parameters! On speed AoA? + aoa.Slow=13.0 + aoa.OnSpeedMax=12 + aoa.OnSpeed=11 + aoa.OnSpeedMin=10 + aoa.Fast=9.0 + end + + +end + --- Get optimal aircraft flight parameters at checkpoint. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -1270,6 +1418,9 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) local aoa local dist local speed + + -- Aircraft specific AoA. + local aoaac=self:_GetAircraftAoA(playerData) if step==AIRBOSS.PatternStep.PLATFORM then @@ -1301,7 +1452,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) dist=-UTILS.NMToMeters(3) - aoa=8.1 + aoa=aoaac.OnSpeed elseif step==AIRBOSS.PatternStep.INITIAL then @@ -1347,7 +1498,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) alt=UTILS.FeetToMeters(500) end - aoa=8.1 + aoa=aoaac.OnSpeed dist=UTILS.NMToMeters(1.2) @@ -1359,7 +1510,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) alt=UTILS.FeetToMeters(500) end - aoa=8.1 + aoa=aoaac.OnSpeed elseif step==AIRBOSS.PatternStep.WAKE then @@ -1369,7 +1520,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) alt=UTILS.FeetToMeters(370) --? end - aoa=8.1 + aoa=aoaac.OnSpeed elseif step==AIRBOSS.PatternStep.FINAL then @@ -1379,7 +1530,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) alt=UTILS.FeetToMeters(300) --? end - aoa=8.1 + aoa=aoaac.OnSpeed end @@ -1501,19 +1652,42 @@ function AIRBOSS:_ScanCarrierZone() -- Create a new flight group if knownflight then - self:I(string.format("Known flight group %s of type %s in CCA.", groupname, actype)) + self:T2(self.lid..string.format("Known flight group %s of type %s in CCA.", groupname, actype)) if knownflight.ai then -- Get distance to carrier. local dist=knownflight.group:GetCoordinate():Get2DDistance(self:GetCoordinate()) - -- Send AI flight to marshal stack if group closes in more than 5 km and has initial flag value. - if knownflight.dist0-dist>UTILS.NMToMeters(5) and knownflight.flag:Get()==-100 then - self:_MarshalAI(knownflight) + -- Close in distance. Is >0 if AC comes closer wrt to first detected distance d0. + local closein=knownflight.dist0-dist + + -- Debug info. + self:T3(self.lid..string.format("Known flight group %s closed in by %.1f NM", knownflight.groupname, UTILS.MetersToNM(closein))) + + -- Send AI flight to marshal stack if group closes in more than 2.5 and has initial flag value. + if closein>UTILS.NMToMeters(2.5) and knownflight.flag:Get()==-100 then + + -- Check that we do not add a recovery tanker for marshaling. + if self.tanker and self.tanker.tanker:GetName()==groupname then + + -- Don't touch the recovery thanker! + + else + + -- Get the next free stack for current recovery case. + local stack=self:_GetFreeStack(self.case) + + -- Send AI to marshal stack. + self:_MarshalAI(knownflight, stack) + + -- Add group to marshal stack queue + self:_AddMarshallGroup(knownflight, stack) + + end end end else - self:I(string.format("UNKNOWN flight group %s of type %s detected inside CCA.", groupname, actype)) + self:I(self.lid..string.format("New flight group %s of type %s detected inside CCA.", groupname, actype)) self:_CreateFlightGroup(group) end @@ -1546,7 +1720,7 @@ function AIRBOSS:_MarshalPlayer(playerData) if playerData then -- Number of flight groups in stack. - local ngroups,nunits=self:_GetQueueInfo(self.Qmarshal, self.case) + local ngroups, nunits=self:_GetQueueInfo(self.Qmarshal, self.case) -- Assign next free stack to this flight. local mystack=ngroups+1 @@ -1568,7 +1742,7 @@ function AIRBOSS:_MarshalPlayer(playerData) -- Flight is not registered yet. local text="you are not yet registered inside the CCA. Marshal request denied!" - self:MessageToPlayer(playerData, text, "AIRBOSS") + self:MessageToPlayer(playerData, text, "MARSHAL") end end @@ -1576,19 +1750,18 @@ end --- Tell AI to orbit at a specified position at a specified alititude with a specified speed. -- @param #AIRBOSS self -- @param #AIRBOSS.Flightitem flight Flight group. -function AIRBOSS:_MarshalAI(flight) +-- @param #number nstack Stack number of group. (Should be #self.Qmarshal+1 for new flight groups.) +function AIRBOSS:_MarshalAI(flight, nstack) -- Flight group name. local group=flight.group local groupname=flight.groupname - - -- Check that we do not add a recovery tanker for marshaling. - if self.tanker and self.tanker.tanker:GetName()==groupname then - return - end - -- Number of already full marshal stacks. - local nstacks=#self.Qmarshal + -- Debug info. + self:I(self.lid..string.format("Sending AI group %s to marshal stack %d. Current stack/flag value=%d.", groupname, nstack, flight.flag:Get())) + + -- Set flag/stack value. + flight.flag:Set(nstack) -- Current carrier position. local Carrier=self:GetCoordinate() @@ -1610,8 +1783,7 @@ function AIRBOSS:_MarshalAI(flight) local wp={} -- Set up waypoints including collapsing the stack. - local n=1 -- Waypoint counter. - for stack=nstacks+1,1,-1 do + for stack=nstack, 1, -1 do -- Get altitude and positions. local Altitude, p1, p2=self:_GetMarshalAltitude(stack) @@ -1626,18 +1798,13 @@ function AIRBOSS:_MarshalAI(flight) local text=string.format("Marshal @ alt=%d ft, dist=%.1f NM, speed=%d knots", UTILS.MetersToFeet(Altitude), UTILS.MetersToNM(Dist), UTILS.MpsToKnots(Speed)) -- Waypoint. - wp[n]=p1:SetAltitude(Altitude):WaypointAirTurningPoint(nil, Speed, {TaskOrbit}, text) + wp[#wp+1]=p1:SetAltitude(Altitude):WaypointAirTurningPoint(nil, Speed, {TaskOrbit}, text) - -- Increase counter. - n=n+1 end -- Landing waypoint. wp[#wp+1]=Carrier:WaypointAirLanding(Speed, self.airbase, nil, "Landing") - - -- Add group to marshal stack. - self:_AddMarshallGroup(flight, nstacks+1) - + -- Reinit waypoints. group:WayPointInitialize(wp) @@ -1714,34 +1881,32 @@ end ---- Add a flight group to the marshal stack. +--- Add a flight group to a specific marshal stack and to the marshal queue. -- @param #AIRBOSS self -- @param #AIRBOSS.Flightitem flight Flight group. --- @param #number flagvalue Initial user flag value = stack number for holding. -function AIRBOSS:_AddMarshallGroup(flight, flagvalue) +-- @param #number stack Marshal stack. This (re-)sets the flag value. +function AIRBOSS:_AddMarshallGroup(flight, stack) -- Set flag value. This corresponds to the stack number which starts at 1. - flight.flag:Set(flagvalue) + flight.flag:Set(stack) -- Set recovery case. flight.case=self.case -- Pressure. local P=UTILS.hPa2inHg(self:GetCoordinate():GetPressure()) - - -- Get unit name for board number. - local unitname=flight.group:GetUnit(1):GetName() - - -- TODO: Get correct board number if possible? - local boardnumber=tostring(flight.onboardnumbers[unitname]) - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(flagvalue, flight.case)) + + -- Stack altitude. + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case)) local brc=self:GetBRC() -- Marshal message. -- TODO: Get charlie time estimate. - local text=string.format("%s, Case %d, BRC is %03d, hold at %d. Expected Charlie Time XX.\n", boardnumber, flight.case, brc, alt) + local text=string.format("%s, Case %d, BRC is %03d, hold at %d. Expected Charlie Time XX.\n", flight.onboard, flight.case, brc, alt) text=text..string.format("Altimeter %.2f. Report see me.", P) - MESSAGE:New(text, 30):ToAll() + + -- Message to all players. + self:MessageToAll(text, "MARSHAL") -- Add to marshal queue. table.insert(self.Qmarshal, flight) @@ -1859,18 +2024,22 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) end end ---- Get next free stack. +--- Get next free stack depending on recovery case. Note that here we assume one flight group per stack! -- @param #AIRBOSS self --- @param #number case Recovery case --- @return #number Smalest (lowest) free stack. +-- @param #number case Recovery case. Default current (self) case in progress. +-- @return #number Lowest free stack available for the given case. function AIRBOSS:_GetFreeStack(case) + -- Recovery case. case=case or self.case + -- Get stack local stack if case==1 then + -- Lowest Case I stack. stack=self:_GetQueueInfo(self.Qmarshal, 1) else + -- Lowest Case II or III stack. stack=self:_GetQueueInfo(self.Qmarshal, 23) end @@ -1880,33 +2049,39 @@ end --- Get number of groups and units in queue. -- @param #AIRBOSS self --- @param #table queue The queue. Can me all, marshal or pattern. --- @param #number case (Optional) Count only flights which are in a specific recovery case. --- @return #number Total Number of flight groups in queue. --- @return #number Total number of aircraft in queue. +-- @param #table queue The queue. Can be self.flights, self.Qmarshal or self.Qpattern. +-- @param #number case (Optional) Only count flights, which are in a specific recovery case. Note that you can use case=23 for flights that are either in Case II or III. By default all groups/units regardless of case are counted. +-- @return #number Total number of flight groups in queue. +-- @return #number Total number of aircraft in queue since each flight group can contain multiple aircraft. function AIRBOSS:_GetQueueInfo(queue, case) local ngroup=0 local nunits=0 + -- Loop over flight groups. for _,_flight in pairs(queue) do local flight=_flight --#AIRBOSS.Flightitem + -- Check if a specific case was requested. if case then - if flight.case==case or (case==23 and (flight.case==2 or flight.case==3)) then + -- Only count specific case with special 23 = CASE II and III combined. + if (flight.case==case) or (case==23 and (flight.case==2 or flight.case==3)) then ngroup=ngroup+1 nunits=nunits+flight.nunits end else + + -- No specific case requested. Count all groups & units in selected queue. ngroup=ngroup+1 - nunits=nunits+flight.nunits + nunits=nunits+flight.nunits + end end - return nunits, ngroup + return ngroup, nunits end --- Print holding queue. @@ -1975,8 +2150,9 @@ function AIRBOSS:_CreateFlightGroup(group) flight.case=self.case -- Onboard - if flight.ai then - flight.onboard=flight.onboardnumbers[flight.seclead] + if flight.ai then + local onboard=flight.onboardnumbers[flight.seclead] + flight.onboard=onboard else flight.onboard=self:_GetOnboardNumberPlayer(group) end @@ -3497,7 +3673,7 @@ end -- -- * Glide slope error > 3 degrees. -- * Line up error > 3 degrees. --- * AoA<6.9 or AoA>9.3. +-- * AoA<6.9 or AoA>9.3 for TOPGUN graduates. -- @param #AIRBOSS self -- @param #number glideslopeError Glide slope error in degrees. -- @param #number lineupError Line up error in degrees. @@ -3510,13 +3686,13 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, difficulty) -- Too high or too low? if math.abs(glideslopeError)>1 then - self:I(self.lid.."Wave off due to glide slope error >1 degree!") + self:I(self.lid..string.format("Wave off due to glide slope error %.1f > 1 degree!", glideslopeError)) waveoff=true end -- Too far from centerline? if math.abs(lineupError)>3 then - self:I(self.lid.."Wave off due to line up error >3 degrees!") + self:I(self.lid..string.format("Wave off due to line up error %.1f > 3 degrees!", lineupError)) waveoff=true end @@ -3742,7 +3918,9 @@ function AIRBOSS:_GetZoneArcIn(case) end - local x=12/math.cos(math.rad(self.holdingoffset)) + local alpha=math.rad(self.holdingoffset) + + local x=12/math.cos(alpha) -- Distance = 14 NM local distance=UTILS.NMToMeters(x) @@ -3822,10 +4000,10 @@ function AIRBOSS:_GetZoneCorridor(case) self:I(string.format("FF case %d radial = %d", case, radial)) self:I(string.format("FF case %d offset = %d", case, offset)) - -- Width of the box. - local w=UTILS.NMToMeters(2) - -- Length of the box. - local l=UTILS.NMToMeters(10) + -- Width of the box in NM. + local w=2 + -- Length of the box in NM. + local l=10 -- Angle between radial and offset in rad. local alpha=math.rad(self.holdingoffset) @@ -3843,6 +4021,16 @@ function AIRBOSS:_GetZoneCorridor(case) local b=w*math.tan(alpha) local a=C-b + + self:I(string.format("FF w = %.1f NM", w)) + self:I(string.format("FF l = %.1f NM", l)) + self:I(string.format("FF d = %.1f NM", d)) + self:I(string.format("FF y = %.1f NM", y)) + self:I(string.format("FF C = %.1f NM", C)) + self:I(string.format("FF b = %.1f NM", b)) + self:I(string.format("FF a = %.1f NM", a)) + + -- TODO: Still not right! local c={} c[1]=self:GetCoordinate() -- Carrier coordinate c[2]=c[1]:Translate( UTILS.NMToMeters(w/2), radial-90) -- 1 Right of carrier @@ -3851,7 +4039,7 @@ function AIRBOSS:_GetZoneCorridor(case) c[5]=c[4]:Translate( UTILS.NMToMeters(l), offset) -- 10 NM to back wall (angled) c[6]=c[5]:Translate( UTILS.NMToMeters(w), offset+90) -- Back wall (angled) c[7]=c[6]:Translate(-UTILS.NMToMeters(l+a), offset) -- 10+a Back along X & Z - c[8]=c[7]:Translate( UTILS.NMToMeters(y), radial-90) -- Back along X + c[8]=c[7]:Translate( UTILS.NMToMeters(y), radial-90) -- Back along X c[9]=c[1]:Translate( UTILS.NMToMeters(w/2), radial+90) -- 1 left of carrier -- Create an array of a square! @@ -3945,7 +4133,7 @@ function AIRBOSS:_DetailedPlayerStatus(playerData) local yaw=unit:GetYaw() local roll=unit:GetRoll() local pitch=unit:GetPitch() - + -- Distance to the boat. local dist=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) local dx,dz,rho,phi=self:_GetDistances(unit) @@ -4330,25 +4518,28 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) text=text..string.format(" Lineup Error = %.1f°\n", lineupError) - -- Get AoA. + -- Get current AoA. local aoa=playerData.unit:GetAoA() - -- TODO: Generalize AoA for other aircraft! - if aoa>=9.3 then + -- Get aircraft AoA parameters. + local aircraftaoa=self:_GetAircraftAoA(playerData) + + -- Rate aoa. + if aoa>=aircraftaoa.Slow then -- "Your're slow!" self:RadioTransmission(self.LSOradio, self.radiocall.SLOW, true, delay) --delay=delay+1.5 - elseif aoa>=8.8 and aoa<9.3 then + elseif aoa>=aircraftaoa.OnSpeedMax and aoa=7.4 and aoa<8.8 then + elseif aoa>=aircraftaoa.OnSpeedMin and aoa=6.9 and aoa<7.4 then + elseif aoa>=aircraftaoa.Fast and aoa=0 and aoa<6.9 then + elseif aoa/Help - missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _helpPath, self._AttitudeMonitor, self, playername) - missionCommands.addCommandForGroup(_gid, "Smoke Marshal Zone", _helpPath, self._MarkMarshalZone, self, _unitName, false) - missionCommands.addCommandForGroup(_gid, "Flare Marshal Zone", _helpPath, self._MarkMarshalZone, self, _unitName, true) - missionCommands.addCommandForGroup(_gid, "Smoke CASE II/III Zones", _helpPath, self._MarkCase23Zones, self, _unitName, false) - missionCommands.addCommandForGroup(_gid, "Flare CASE II/III Zones", _helpPath, self._MarkCase23Zones, self, _unitName, true) + missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _helpPath, self._AttitudeMonitor, self, playername) + missionCommands.addCommandForGroup(_gid, "Smoke Marshal Zone", _helpPath, self._MarkMarshalZone, self, _unitName, false) + missionCommands.addCommandForGroup(_gid, "Flare Marshal Zone", _helpPath, self._MarkMarshalZone, self, _unitName, true) + missionCommands.addCommandForGroup(_gid, "Smoke R. Case Zones", _helpPath, self._MarkCase23Zones, self, _unitName, false) + missionCommands.addCommandForGroup(_gid, "Flare R. Case Zones", _helpPath, self._MarkCase23Zones, self, _unitName, true) missionCommands.addCommandForGroup(_gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) -- F10/Airboss//Kneeboard @@ -6053,6 +6244,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocityMPS()) -- Tacan/ICLS. + -- TODO: adjust to option TACAN or ICLS disabled. local tacan="unknown" local icls="unknown" if self.TACANchannel~=nil then @@ -6066,14 +6258,28 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) local Nmarshal,nmarshal=self:_GetQueueInfo(self.Qmarshal, playerData.case) local Npattern,npattern=self:_GetQueueInfo(self.Qpattern) + -- Current abs time. + local Tabs=timer.getAbsTime() + -- Get recovery times of carrier. - local recoverytimes="Recovery time slots:" - if #self.recoverytime==0 then - recoverytimes=recoverytimes.." empty" + local recoverytext="Recovery time windows (max 5):" + if #self.recoverytimes==0 then + recoverytext=recoverytext.." none!" else - for _,_rtime in pairs(self.recoverytime) do - local rtime=_rtime --#AIRBOSS.Recovery - recoverytimes=recoverytimes..string.format("\nSlot %s - %s", UTILS.SecondsToClock(rtime.START), UTILS.SecondsToClock(rtime.STOP)) + -- Loop over recovery windows. + local rw=0 + for _,_recovery in pairs(self.recoverytimes) do + local recovery=_recovery --#AIRBOSS.Recovery + -- Only include current and future recovery windows. + if Tabs=5 then + -- Break the loop after 5 recovery times. + break + end + end end end @@ -6081,7 +6287,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) local text=string.format("%s info:\n", self.alias) text=text..string.format("=============================================\n") text=text..string.format("Carrier state %s\n", self:GetState()) - text=text..string.format("Case %d Recovery\n", self.case) + text=text..string.format("Case %d recovery\n", self.case) text=text..string.format("BRC %03d°\n", self:GetBRC()) text=text..string.format("FB %03d°\n", self:GetFinalBearing(true)) text=text..string.format("Speed %d kts\n", carrierspeed) @@ -6092,7 +6298,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) text=text..string.format("# A/C total %d\n", #self.flights) text=text..string.format("# A/C marshal %d (%d)\n", Nmarshal, nmarshal) text=text..string.format("# A/C pattern %d (%d)\n", Npattern, npattern) - text=text..string.format(recoverytimes) + text=text..string.format(recoverytext) self:T2(self.lid..text) -- Send message. @@ -6292,47 +6498,86 @@ function AIRBOSS:_MarkCase23Zones(_unitName, flare) if playerData then + -- Player's recovery case. local case=playerData.case - if case<2 then - case=3 - end - -- Initial local text=string.format("Marking CASE %d zone:\n", case) - --TODO: Add height! + --TODO: Add height - at least in some cases? if flare then + + -- Case I/II: Initial + text=text.."* initial with WHITE flares\n" + self.zoneInitial:FlareZone(FLARECOLOR.White, 45) + + -- Case II/III: approach corridor text=text.."* approach corridor with GREEN flares\n" self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) + + -- Case II/III: platform text=text.."* platform with RED flares\n" self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red, 45) + + -- Case III: dirty up text=text.."* dirty up with YELLOW flares\n" self:_GetZoneDirtyUp(case):FlareZone(FLARECOLOR.Yellow, 45) + + -- Case II/III: arc in/out if math.abs(self.holdingoffset)>0 then self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.Yellow, 45) text=text.."* arc turn in with YELLOW flares\n" self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White, 45) text=text.."* arc trun out with WHITE flares\n" end + + -- Case III: bullseye text=text.."* bullseye with WHITE flares\n" self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.White, 45) + else - text=text.."* approach corridor with GREEN smoke\n" - self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) - text=text.."* platform with RED smoke\n" - self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) - text=text.."* dirty up with ORANGE flares\n" - self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) - if math.abs(self.holdingoffset)>0 then - self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Red, 45) - text=text.."* arc turn in with YELLOW flares\n" - self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Orange, 45) - text=text.."* arc trun out with WHITE flares\n" + + -- Case I/II: Initial + if case==1 or case==2 or self.Debug then + text=text.."* initial with WHITE smoke\n" + self.zoneInitial:SmokeZone(SMOKECOLOR.White, 45) end - text=text.."* bullseye with BLUE smoke\n" - self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Blue, 45) + + -- Case II/III: Approach Corridor + if case==2 or case==3 or self.Debug then + text=text.."* approach corridor with GREEN smoke\n" + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + -- Case II/III: platform + if case==2 or case==3 or self.Debug then + text=text.."* platform with RED smoke\n" + self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) + end + + -- Case II/III: arc in/out if offset>0. + if case==2 or case==3 or self.Debug then + if math.abs(self.holdingoffset)>0 then + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Red, 45) + text=text.."* arc turn in with YELLOW flares\n" + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Orange, 45) + text=text.."* arc trun out with WHITE flares\n" + end + end + + -- Case III: dirty up + if case==3 or self.Debug then + text=text.."* dirty up with ORANGE flares\n" + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + end + + -- Case III: dirty up + if case==3 or self.Debug then + text=text.."* bullseye with BLUE smoke\n" + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Blue, 45) + end + end -- Send message to player. diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 54068e7fb..f3dd49e54 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -214,9 +214,10 @@ RECOVERYTANKER.version="0.9.4w" -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Seamless change of position update. Get good updated waypoint and update position if tanker position is right! -- TODO: Check if TACAN mode "X" is allowed for AA TACAN stations. -- TODO: Check if tanker is going back to "Running" state after RTB and respawn. --- TODO: Is alive check for tanker. +-- TODO: Is alive check for tanker necessary? -- DONE: Write documenation. -- DONE: Trace functions self:T instead of self:I for less output. -- DONE: Make pattern update parameters (distance, orientation) input parameters. From 74d97cc220c238332f7bde4188396a3bfa66e6c1 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 2 Dec 2018 23:48:39 +0100 Subject: [PATCH 072/147] AIRBOSS v0.4.3 --- Moose Development/Moose/Ops/Airboss.lua | 511 ++++++++++++++---------- 1 file changed, 306 insertions(+), 205 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 87cc8a3bb..4fc8def7b 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -20,13 +20,14 @@ -- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much work in progress. -- -- At the moment, parameters are optimized for F/A-18C Hornet as aircraft and USS John C. Stennis as carrier. --- The community mod A-4E is also supported in priciple but needs further tweaking of parameters suche as on speed AoA values. +-- The community A-4E-C mod is also supported in priciple but needs further tweaking of parameters suche as on speed AoA values. +-- -- Other aircraft and carriers **might** be possible in future but would need a different set of optimized parameters. -- -- === -- -- ### Author: **funkyfranky** --- ### Co-author: **Bankler** (Carrier trainer idea and script) +-- ### Special Thanks To: **Bankler** (Carrier trainer idea and script) -- -- @module Ops.Airboss -- @image MOOSE.JPG @@ -47,7 +48,6 @@ -- @field #number ICLSchannel ICLS channel. -- @field Core.Radio#RADIO LSOradio Radio for LSO calls. -- @field Core.Radio#RADIO Carrierradio Radio for carrier calls. --- @field #AIRBOSS.RadioCalls radiocall LSO and Airboss call sound files and texts. -- @field Core.Scheduler#SCHEDULER radiotimer Radio queue scheduler. -- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. @@ -132,7 +132,6 @@ AIRBOSS = { LSOfreq = nil, Carrierradio = nil, Carrierfreq = nil, - radiocall = {}, radiotimer = nil, zoneCCA = nil, zoneCCZ = nil, @@ -258,99 +257,194 @@ AIRBOSS.PatternStep={ --- Radio sound file and subtitle. -- @type AIRBOSS.RadioSound --- @field #string normal Sound file normal. --- @field #string louder Sound file loud. +-- @field #string file Sound file name without suffix. +-- @field #string suffix File suffix/extention, e.g. "ogg". +-- @field #boolean loud Loud version of sound file available. -- @field #string subtitle Subtitle displayed during transmission. --- @field #number duration Duration in seconds the subtitle is displayed. +-- @field #number duration Duration of the sound in seconds. This is also the duration the subtitle is displayed. ---- LSO and Airboss radio calls. --- @type AIRBOSS.RadioCalls +--- LSO radio calls. +-- @type AIRBOSS.LSOCall -- @field #AIRBOSS.RadioSound RIGHTFORLINEUP "Right for line up!" call. -- @field #AIRBOSS.RadioSound COMELEFT "Come left!" call. -- @field #AIRBOSS.RadioSound HIGH "You're high!" call. --- @field #AIRBOSS.RadioSound POWER Sound file "Power!" call. --- @field #AIRBOSS.RadioSound SLOW Sound file "You're slow!" call. --- @field #AIRBOSS.RadioSound FAST Sound file "You're fast!" call. --- @field #AIRBOSS.RadioSound CALLTHEBALL Sound file "Call the ball." call. --- @field #AIRBOSS.RadioSound ROGERBALL "Roger, ball." call. --- @field #AIRBOSS.RadioSound WAVEOFF "Wave off!" call. --- @field #AIRBOSS.RadioSound BOLTER "Bolter, bolter!" call. +-- @field #AIRBOSS.RadioSound POWER "Power!" call. +-- @field #AIRBOSS.RadioSound CALLTHEBALL "Call the Ball" +-- @field #AIRBOSS.RadioSound ROGERBALL "Roger ball" call (actually from pilot). +-- @field #AIRBOSS.RadioSound WAVEOFF "Wafe off" call +-- @field #AIRBOSS.RadioSound BOLTER "Bolter, Bolter" call -- @field #AIRBOSS.RadioSound LONGINGROOVE "You're long in the groove. Depart and re-enter." call. - ---- Default radio call sound files. --- @type AIRBOSS.Soundfile --- @field #AIRBOSS.RadioSound RIGHTFORLINEUP --- @field #AIRBOSS.RadioSound COMELEFT --- @field #AIRBOSS.RadioSound HIGH --- @field #AIRBOSS.RadioSound POWER --- @field #AIRBOSS.RadioSound CALLTHEBALL --- @field #AIRBOSS.RadioSound ROGERBALL --- @field #AIRBOSS.RadioSound WAVEOFF --- @field #AIRBOSS.RadioSound BOLTER --- @field #AIRBOSS.RadioSound LONGINGROOVE -AIRBOSS.Soundfile={ +-- @field #AIRBOSS.RadioSound N1 "One" call. +-- @field #AIRBOSS.RadioSound N2 "Two" call. +-- @field #AIRBOSS.RadioSound N9 "Nine" call. +AIRBOSS.LSOCall={ RIGHTFORLINEUP={ - normal="LSO - RightLineUp(S).ogg", - louder="LSO - RightLineUp(L).ogg", - subtitle="Right for line up.", - duration=1.5, + file="LSO-RightForLineup", + suffix="ogg", + loud=true, + subtitle="Right for line up", + duration=1.0, }, COMELEFT={ - normal="LSO - ComeLeft(S).ogg", - louder="LSO - ComeLeft(L).ogg", - subtitle="Come left.", - duration=1, + file="LSO-ComeLeft", + suffix="ogg", + loud=true, + subtitle="Come left", + duration=0.8, }, HIGH={ - normal="LSO - High(S).ogg", - louder="LSO - High(L).ogg", - subtitle="You're high.", - duration=1, + file="LSO-High", + loud=true, + subtitle="You're high", + duration=0.9, + }, + LOW={ + file="LSO-Low", + loud=true, + subtitle="You're low", + duration=0.6, }, POWER={ - normal="LSO - Power(S).ogg", - louder="LSO - Power(L).ogg", - subtitle="Power.", - duration=1, + file="LSO-Power", + suffix="ogg", + loud=true, + subtitle="Power", + duration=0.6, }, SLOW={ - normal="LSO-Slow-Normal.ogg", - louder="LSO-Slow-Loud.ogg", - subtitle="You're slow.", - duration=1, + file="LSO-Slow", + suffix="ogg", + loud=true, + subtitle="You're slow", + duration=0.9, }, FAST={ - normal="LSO-Fast-Normal.ogg", - louder="LSO-Fast-Loud.ogg", - subtitle="You're fast.", - duration=1, + file="LSO-Fast", + suffix="ogg", + loud=true, + subtitle="You're fast", + duration=0.9, }, CALLTHEBALL={ - normal="LSO - Call the Ball.ogg", - louder="LSO - Call the Ball.ogg", - subtitle="Call the ball.", - duration=3, + file="LSO-CallTheBall", + suffix="ogg", + louder=false, + subtitle="Call the ball", + duration=0.7, }, ROGERBALL={ - normal="LSO - Roger.ogg", - subtitle="Roger ball!", - duration=1.2, + file="LSO-RogerBall", + suffix="ogg", + louder=false, + subtitle="Roger ball", + duration=0.7, }, WAVEOFF={ - normal="LSO - WaveOff.ogg", - subtitle="Wave off!", - duration=1, + file="LSO-WaveOff", + suffix="ogg", + louder=false, + subtitle="Wave off", + duration=0.7, }, BOLTER={ - normal="LSO - Bolter.ogg", + file="LSO-BolterBolter", + suffix="ogg", + louder=false, subtitle="Bolter, Bolter!", - duration=1.5, + duration=1.0, }, LONGINGROOVE={ - normal="LSO - Long in Groove.ogg", - subtitle="You're long in the groove. Depart and re-enter.", - duration=3, - } + file="LSO-LonInTheGroove", + suffix="ogg", + louder=false, + subtitle="You're long in the groove", + duration=1.3, + }, + DEPARTANDREENTER={ + file="LSO-DepartAndReenter", + suffix="ogg", + louder=false, + subtitle="Depart and re-enter", + duration=1.3, + }, + PADDLESCONTACT={ + file="LSO-PaddlesContact", + suffix="ogg", + louder=false, + subtitle="Paddles, contact", + duration=1.0, + }, + N1={ + file="LSO-N1", + suffix="ogg", + louder=false, + subtitle="", + duration=0.3, + }, + N2={ + file="LSO-N2", + suffix="ogg", + louder=false, + subtitle="", + duration=0.3, + }, + N3={ + file="LSO-N3", + suffix="ogg", + louder=false, + subtitle="", + duration=0.4, + }, + N4={ + file="LSO-N4", + suffix="ogg", + louder=false, + subtitle="", + duration=0.4, + }, + N5={ + file="LSO-N5", + suffix="ogg", + louder=false, + subtitle="", + duration=0.4, + }, + N6={ + file="LSO-N6", + suffix="ogg", + louder=false, + subtitle="", + duration=0.6, + }, + N7={ + file="LSO-N7", + suffix="ogg", + louder=false, + subtitle="", + duration=0.6, + }, + N8={ + file="LSO-N8", + suffix="ogg", + louder=false, + subtitle="", + duration=0.4, + }, + N9={ + file="LSO-N9", + suffix="ogg", + louder=false, + subtitle="", + duration=0.5, + }, + N0={ + file="LSO-N0", + suffix="ogg", + louder=false, + subtitle="", + duration=0.4, + }, + } --- Difficulty level. @@ -366,8 +460,8 @@ AIRBOSS.Difficulty={ --- Recovery time. -- @type AIRBOSS.Recovery --- @field #number START Start of recovery. --- @field #number STOP End of recovery. +-- @field #number START Start of recovery in seconds of abs time. +-- @field #number STOP End of recovery in seconds of abs time. -- @field #number CASE Recovery case (1-3) of that time slot. --- Groove position. @@ -438,7 +532,7 @@ AIRBOSS.GroovePos={ -- @field #boolean player If true, flight is a human player. -- @field #string actype Aircraft type name. -- @field #table onboardnumbers Onboard numbers of aircraft in the group. --- @field #number onboard Onboard number of player or fist unit in group. +-- @field #string onboard Onboard number of player or first unit in group. -- @field #number case Recovery case of flight. -- @field #string seclead Name of section lead. -- @field #table section Other human flight groups belonging to this flight. This flight is the lead. @@ -473,7 +567,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.2" +AIRBOSS.version="0.4.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -620,11 +714,14 @@ function AIRBOSS:New(carriername, alias) self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) end - +--[[ -- Init default sound files. - for _name,_sound in pairs(AIRBOSS.Soundfile) do + for _name,_sound in pairs(AIRBOSS.LSOCall) do local sound=_sound --#AIRBOSS.RadioSound - self.radiocall[_name]=sound + local text=string.format() + sound.subtitle=1 + sound.louder=1 + --self.radiocall[_name]=sound end -- Debug: @@ -632,6 +729,7 @@ function AIRBOSS:New(carriername, alias) for _name,_sound in pairs(self.radiocall) do self:T{name=_name,sound=_sound} end +]] ----------------------- --- FSM Transitions --- @@ -1071,75 +1169,7 @@ function AIRBOSS:_CheckRecoveryTimes() end -- Debug output. - self:I(self.lid..text) - - --[[ - - -- Check if a recovery time was set. - if #self.recoverytime==0 then - - -- If no recovery times have been specified, we assume any time is okay. - self:I("FF Start recovery. No recovery time set!") - if not self:IsRecovering() then - -- Give command to recover! - return true - else - -- Do nothing. - return nil - end - - else - - -- Selected recovery event if any. - local recovery=nil --#AIRBOSS.Recovery - - for i,_rtime in pairs(self.recoverytime) do - local rtime=_rtime --#AIRBOSS.Recovery - - if abstime>=rtime.START and abstime<=rtime.STOP then - - -- This is a valid time slot. Do not touch recovery again! - recovery=rtime - - elseif abstime>rtime.STOP then - -- Stop time has already passed. - -- We do not remove the time sind the above #recovery check would fail by automatically setting case 1 always! - --table.insert(remove, i) - elseif abstime1 then -- "You're high!" - self:RadioTransmission(self.LSOradio, self.radiocall.HIGH, true, delay) - --delay=delay+1.5 + self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.HIGH, true) elseif glideslopeError>0.5 then -- "You're a little high." - self:RadioTransmission(self.LSOradio, self.radiocall.HIGH, false, delay) - --delay=delay+1.5 + self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.HIGH, false) elseif glideslopeError<-1.0 then -- "Power!" - self:RadioTransmission(self.LSOradio, self.radiocall.POWER, true, delay) - --delay=delay+1.5 + self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.POWER, true) elseif glideslopeError<-0.5 then -- "You're a little low." - self:RadioTransmission(self.LSOradio, self.radiocall.POWER, false, delay) - --delay=delay+1.5 + self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.POWER, false) else text="Good altitude." end @@ -4498,20 +4528,16 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- Lineup left/right calls. if lineupError<-3 then -- "Come left!" - self:RadioTransmission(self.LSOradio, self.radiocall.COMELEFT, true, delay) - --delay=delay+1.5 + self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.COMELEFT, true) elseif lineupError<-1 then -- "Come left." - self:RadioTransmission(self.LSOradio, self.radiocall.COMELEFT, false, delay) - --delay=delay+1.5 + self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.COMELEFT, false) elseif lineupError>3 then -- "Right for lineup!" - self:RadioTransmission(self.LSOradio, self.radiocall.RIGHTFORLINEUP, true, delay) - --delay=delay+1.5 + self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.RIGHTFORLINEUP, true) elseif lineupError>1 then -- "Right for lineup." - self:RadioTransmission(self.LSOradio, self.radiocall.RIGHTFORLINEUP, false, delay) - --delay=delay+1.5 + self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.RIGHTFORLINEUP, false) else text=text.."Good lineup." end @@ -4527,22 +4553,18 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- Rate aoa. if aoa>=aircraftaoa.Slow then -- "Your're slow!" - self:RadioTransmission(self.LSOradio, self.radiocall.SLOW, true, delay) - --delay=delay+1.5 + self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.SLOW, true) elseif aoa>=aircraftaoa.OnSpeedMax and aoa=aircraftaoa.OnSpeedMin and aoa=aircraftaoa.Fast and aoa0 then SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) @@ -5652,6 +5690,69 @@ function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay) end +--- Convert a number (as string) into a radio message. +-- E.g. for board number or headings. +-- @param #AIRBOSS self +-- @param Core.Radio#RADIO radio Radio used for transmission. +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +function AIRBOSS:_Number2Sound(radio, number, delay) + + --- Split string into characters. + local function _split(str) + local chars={} + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + return chars + end + + local alias=radio:GetAlias() + local sender="" + if alias=="LSO" then + sender="LSOCall" + elseif alias=="MARSHAL" then + sender="MarshalCall" + elseif alias=="AIRBOSS" then + sender="AirbossCall" + end + + -- Split string into characters. + local numbers=_split(number) + + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + if n=="0" then + self:RadioTransmission(radio, AIRBOSS[sender].N0, false, delay) + elseif n=="1" then + self:RadioTransmission(radio, AIRBOSS[sender].N1, false, delay) + elseif n=="2" then + self:RadioTransmission(radio, AIRBOSS[sender].N2, false, delay) + elseif n=="3" then + self:RadioTransmission(radio, AIRBOSS[sender].N3, false, delay) + elseif n=="4" then + self:RadioTransmission(radio, AIRBOSS[sender].N4, false, delay) + elseif n=="5" then + self:RadioTransmission(radio, AIRBOSS[sender].N5, false, delay) + elseif n=="6" then + self:RadioTransmission(radio, AIRBOSS[sender].N6, false, delay) + elseif n=="7" then + self:RadioTransmission(radio, AIRBOSS[sender].N7, false, delay) + elseif n=="8" then + self:RadioTransmission(radio, AIRBOSS[sender].N8, false, delay) + elseif n=="9" then + self:RadioTransmission(radio, AIRBOSS[sender].N9, false, delay) + else + self:E(self.lid..string.format("ERROR: Unknown number %s", tostring(n))) + end + end + +end + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- RADIO MENU Functions ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -6539,25 +6640,25 @@ function AIRBOSS:_MarkCase23Zones(_unitName, flare) else -- Case I/II: Initial - if case==1 or case==2 or self.Debug then + if case==1 or case==2 then text=text.."* initial with WHITE smoke\n" self.zoneInitial:SmokeZone(SMOKECOLOR.White, 45) end -- Case II/III: Approach Corridor - if case==2 or case==3 or self.Debug then + if case==2 or case==3 then text=text.."* approach corridor with GREEN smoke\n" self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) end -- Case II/III: platform - if case==2 or case==3 or self.Debug then + if case==2 or case==3 then text=text.."* platform with RED smoke\n" self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) end -- Case II/III: arc in/out if offset>0. - if case==2 or case==3 or self.Debug then + if case==2 or case==3 then if math.abs(self.holdingoffset)>0 then self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Red, 45) text=text.."* arc turn in with YELLOW flares\n" @@ -6567,13 +6668,13 @@ function AIRBOSS:_MarkCase23Zones(_unitName, flare) end -- Case III: dirty up - if case==3 or self.Debug then + if case==3 then text=text.."* dirty up with ORANGE flares\n" self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) end -- Case III: dirty up - if case==3 or self.Debug then + if case==3 then text=text.."* bullseye with BLUE smoke\n" self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Blue, 45) end From 51de6756a783322aac4da18d411e55ecbd73a802 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 3 Dec 2018 23:07:08 +0100 Subject: [PATCH 073/147] AIRBOSS update --- Moose Development/Moose/Ops/Airboss.lua | 67 +++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 4fc8def7b..73dd8eeec 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -374,6 +374,13 @@ AIRBOSS.LSOCall={ subtitle="Paddles, contact", duration=1.0, }, + RADIOCHECK={ + file="LSO-RadioCheck", + suffix="ogg", + louder=false, + subtitle="Paddles, radio check", + duration=1.0, + }, N1={ file="LSO-N1", suffix="ogg", @@ -4963,15 +4970,29 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) -- Altitude error +-X% local _error=(altitude-altopt)/altopt*100 + local radiocall={} --#AIRBOSS.RadioSound + local hint if _error>badscore then hint=string.format("You're high.") + radiocall=AIRBOSS.LSOCall.HIGH + radiocall.loud=true + radiocall.subtitle="" elseif _error>lowscore then hint= string.format("You're slightly high.") + radiocall=AIRBOSS.LSOCall.HIGH + radiocall.loud=false + radiocall.subtitle="" elseif _error<-badscore then hint=string.format("You're low. ") + radiocall=AIRBOSS.LSOCall.LOW + radiocall.loud=true + radiocall.subtitle="" elseif _error<-lowscore then hint=string.format("You're slightly low.") + radiocall=AIRBOSS.LSOCall.LOW + radiocall.loud=false + radiocall.subtitle="" else hint=string.format("Good altitude. ") end @@ -4991,7 +5012,7 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) return hint, debrief end ---- Evaluate player's altitude at checkpoint. +--- Evaluate player's distance to the boat at checkpoint. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -- @param #number optdist Optimal distance in meters. @@ -5831,8 +5852,9 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "Request Marshal?", _rootPath, self._RequestMarshal, self, _unitName) missionCommands.addCommandForGroup(_gid, "Commencing!", _rootPath, self._RequestCommence, self, _unitName) missionCommands.addCommandForGroup(_gid, "Request Refueling?", _rootPath, self._RequestRefueling, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Set Section!", _rootPath, self._SetSection, self, _unitName) - + missionCommands.addCommandForGroup(_gid, "Set Section!", _rootPath, self._SetSection, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Radio Check LSO", _rootPath, self._LSORadioCheck, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Radio Check Marshal", _rootPath, self._MarshalRadioCheck,self, _unitName) end else self:T(self.lid.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) @@ -5881,6 +5903,45 @@ function AIRBOSS:_ResetPlayerStatus(_unitName) end end +--- LSO radio check. Will broadcase LSO message at given LSO frequency. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_LSORadioCheck(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then + -- Broadcase LSO radio check message on LSO radio. + self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.RADIOCHECK) + end + end +end + +--- Marshal radio check. Will broadcase Marshal message at given Marshal frequency. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_MarshalRadioCheck(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then + -- Broadcase LSO radio check message on LSO radio. + -- TODO: Replace LSO message by marshal message. + self:RadioTransmission(self.Carrierradio, AIRBOSS.LSOCall.RADIOCHECK) + end + end +end + --- Request marshal. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. From d66028a1b9bd6f2fba4c3cc03a958503833945e3 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 4 Dec 2018 11:37:07 +0100 Subject: [PATCH 074/147] AIRBOSS v0.4.3w --- Moose Development/Moose/Ops/Airboss.lua | 347 ++++++++++++++---- .../Moose/Ops/RecoveryTanker.lua | 80 +++- 2 files changed, 355 insertions(+), 72 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 4fc8def7b..ed5341d5c 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -10,8 +10,8 @@ -- * Different skill levels from on-the-fly tips for flight students to ziplip for pros. -- * Define recovery time windows with individual recovery cases. -- * Automatic TACAN and ICLS channel setting of carrier. --- * Separate radio channels for LSO and Marshal/Airboss transmissions. --- * Voice over support for LSO, Marshal and Airboss radio transmissions. +-- * Separate radio channels for LSO and Marshal transmissions. +-- * Voice over support for LSO and Marshal and Airboss radio transmissions. -- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, help function (player aircraft attitude, marking of pattern zones etc). -- * Recovery tanker and refueling option via integration of @{#Ops.RecoveryTanker} class. -- * Rescue helo option via @{#Ops.RescueHelo} class. @@ -20,7 +20,7 @@ -- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much work in progress. -- -- At the moment, parameters are optimized for F/A-18C Hornet as aircraft and USS John C. Stennis as carrier. --- The community A-4E-C mod is also supported in priciple but needs further tweaking of parameters suche as on speed AoA values. +-- The community A-4E mod is also supported in priciple but maybe needs further tweaking of parameters such as on speed AoA values. -- -- Other aircraft and carriers **might** be possible in future but would need a different set of optimized parameters. -- @@ -43,11 +43,19 @@ -- @field #string alias Alias of the carrier. -- @field Wrapper.Airbase#AIRBASE airbase Carrier airbase object. -- @field Core.Radio#BEACON beacon Carrier beacon for TACAN and ICLS. +-- @field #boolean TACANon Automatic TACAN is activated. -- @field #number TACANchannel TACAN channel. -- @field #string TACANmode TACAN mode, i.e. "X" or "Y". +-- @field #string TACANmorse TACAN morse code, e.g. "STN". +-- @field #boolean ICLSon Automatic ICLS is activated. -- @field #number ICLSchannel ICLS channel. +-- @field #string ICLSmorse ICLS morse code, e.g. "STN". -- @field Core.Radio#RADIO LSOradio Radio for LSO calls. +-- @field #number LSOfreq LSO radio frequency in MHz. +-- @field #string LSOmodulation LSO radio modulation "AM" or "FM". -- @field Core.Radio#RADIO Carrierradio Radio for carrier calls. +-- @field #number Carrierfreq Marshal radio frequency in MHz. +-- @field #string Carriermodulation Marshal radio modulation "AM" or "FM". -- @field Core.Scheduler#SCHEDULER radiotimer Radio queue scheduler. -- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. @@ -125,13 +133,19 @@ AIRBOSS = { alias = nil, airbase = nil, beacon = nil, + TACANon = nil, TACANchannel = nil, TACANmode = nil, + TACANmorse = nil, + ICLSon = nil, ICLSchannel = nil, + ICLSmorse = nil, LSOradio = nil, LSOfreq = nil, + LSOmodulation = nil, Carrierradio = nil, Carrierfreq = nil, + Carriermodulation = nil, radiotimer = nil, zoneCCA = nil, zoneCCZ = nil, @@ -265,18 +279,30 @@ AIRBOSS.PatternStep={ --- LSO radio calls. -- @type AIRBOSS.LSOCall --- @field #AIRBOSS.RadioSound RIGHTFORLINEUP "Right for line up!" call. --- @field #AIRBOSS.RadioSound COMELEFT "Come left!" call. --- @field #AIRBOSS.RadioSound HIGH "You're high!" call. --- @field #AIRBOSS.RadioSound POWER "Power!" call. +-- @field #AIRBOSS.RadioSound RIGHTFORLINEUP "Right for line up" call. +-- @field #AIRBOSS.RadioSound COMELEFT "Come left" call. +-- @field #AIRBOSS.RadioSound HIGH "You're high" call. +-- @field #AIRBOSS.RadioSound LOW "You're low" call. +-- @field #AIRBOSS.RadioSound POWER "Power" call. +-- @field #AIRBOSS.RadioSound FAST "You're fast" call. +-- @field #AIRBOSS.RadioSound SLOW "You're slow" call. +-- @field #AIRBOSS.RadioSound PADDLESCONTACT "Paddles, contact" call. -- @field #AIRBOSS.RadioSound CALLTHEBALL "Call the Ball" -- @field #AIRBOSS.RadioSound ROGERBALL "Roger ball" call (actually from pilot). -- @field #AIRBOSS.RadioSound WAVEOFF "Wafe off" call -- @field #AIRBOSS.RadioSound BOLTER "Bolter, Bolter" call -- @field #AIRBOSS.RadioSound LONGINGROOVE "You're long in the groove. Depart and re-enter." call. +-- @field #AIRBOSS.RadioSound DEPARTANDREENTER "Depart and re-enter" call. -- @field #AIRBOSS.RadioSound N1 "One" call. -- @field #AIRBOSS.RadioSound N2 "Two" call. +-- @field #AIRBOSS.RadioSound N3 "Three" call. +-- @field #AIRBOSS.RadioSound N4 "Four" call. +-- @field #AIRBOSS.RadioSound N5 "Five" call. +-- @field #AIRBOSS.RadioSound N6 "Six" call. +-- @field #AIRBOSS.RadioSound N7 "Seven" call. +-- @field #AIRBOSS.RadioSound N8 "Eight" call. -- @field #AIRBOSS.RadioSound N9 "Nine" call. +-- @field #AIRBOSS.RadioSound N0 "Zero" call. AIRBOSS.LSOCall={ RIGHTFORLINEUP={ file="LSO-RightForLineup", @@ -444,7 +470,91 @@ AIRBOSS.LSOCall={ subtitle="", duration=0.4, }, +} +--- Marshal radio calls. +-- @type AIRBOSS.MarshalCall +-- @field #AIRBOSS.RadioSound N1 "One" call. +-- @field #AIRBOSS.RadioSound N2 "Two" call. +-- @field #AIRBOSS.RadioSound N3 "Three" call. +-- @field #AIRBOSS.RadioSound N4 "Four" call. +-- @field #AIRBOSS.RadioSound N5 "Five" call. +-- @field #AIRBOSS.RadioSound N6 "Six" call. +-- @field #AIRBOSS.RadioSound N7 "Seven" call. +-- @field #AIRBOSS.RadioSound N8 "Eight" call. +-- @field #AIRBOSS.RadioSound N9 "Nine" call. +-- @field #AIRBOSS.RadioSound N0 "Zero" call. +AIRBOSS.MarshalCall={ + N1={ + file="LSO-N1", + suffix="ogg", + louder=false, + subtitle="", + duration=0.3, + }, + N2={ + file="LSO-N2", + suffix="ogg", + louder=false, + subtitle="", + duration=0.3, + }, + N3={ + file="LSO-N3", + suffix="ogg", + louder=false, + subtitle="", + duration=0.4, + }, + N4={ + file="LSO-N4", + suffix="ogg", + louder=false, + subtitle="", + duration=0.4, + }, + N5={ + file="LSO-N5", + suffix="ogg", + louder=false, + subtitle="", + duration=0.4, + }, + N6={ + file="LSO-N6", + suffix="ogg", + louder=false, + subtitle="", + duration=0.6, + }, + N7={ + file="LSO-N7", + suffix="ogg", + louder=false, + subtitle="", + duration=0.6, + }, + N8={ + file="LSO-N8", + suffix="ogg", + louder=false, + subtitle="", + duration=0.4, + }, + N9={ + file="LSO-N9", + suffix="ogg", + louder=false, + subtitle="", + duration=0.5, + }, + N0={ + file="LSO-N0", + suffix="ogg", + louder=false, + subtitle="", + duration=0.4, + }, } --- Difficulty level. @@ -567,7 +677,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.3" +AIRBOSS.version="0.4.3w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -905,6 +1015,12 @@ function AIRBOSS:AddRecoveryTime(starttime, stoptime, case) return self end +--- Disable automatic TACAN activation +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetTACANoff() + self.TACANon=false +end --- Set TACAN channel of carrier. -- @param #AIRBOSS self @@ -912,21 +1028,33 @@ end -- @param #string mode TACAN mode, i.e. "X" or "Y". Default "X". -- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". -- @return #AIRBOSS self -function AIRBOSS:SetTACAN(channel, mode, moresecode) +function AIRBOSS:SetTACAN(channel, mode, morsecode) self.TACANchannel=channel or 74 self.TACANmode=mode or "X" - + self.TACANmorse=morsecode or "STN" + self.TACANon=true + return self end +--- Disable automatic ICLS activation. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetICLSoff() + self.ICLSon=false +end + --- Set ICLS channel of carrier. -- @param #AIRBOSS self -- @param #number channel ICLS channel. Default 1. +-- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". -- @return #AIRBOSS self -function AIRBOSS:SetICLS(channel) +function AIRBOSS:SetICLS(channel, morsecode) self.ICLSchannel=channel or 1 + self.ICLSmorse=morsecode or "STN" + self.ICLSon=true return self end @@ -1036,13 +1164,13 @@ function AIRBOSS:onafterStart(From, Event, To) self:I(self.lid..string.format("Theatre = %s", tostring(theatre))) -- Activate TACAN. - if self.TACANchannel~=nil and self.TACANmode~=nil then - self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, "STN", true) + if self.TACANon then + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) end -- Activate ICLS. - if self.ICLSchannel then - self.beacon:ActivateICLS(self.ICLSchannel, "STN") + if self.ICLSon then + self.beacon:ActivateICLS(self.ICLSchannel, self.ICLSmorse) end -- Handle events. @@ -1399,6 +1527,7 @@ function AIRBOSS:_GetAircraftAoA(playerData) local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B + -- Table with AoA values. local aoa={} -- #AIRBOSS.AircraftAoA if hornet then @@ -1761,11 +1890,16 @@ function AIRBOSS:_MarshalPlayer(playerData) -- Set step to holding. playerData.step=AIRBOSS.PatternStep.HOLDING + playerData.warning=nil + + -- Holding switch to nil until player arrives in the holding zone. + playerData.holding=nil -- Set same stack for all flights in section. for _,_flight in pairs(playerData.section) do local flight=_flight --#AIRBOSS.PlayerData flight.step=AIRBOSS.PatternStep.HOLDING + flight.holding=nil flight.flag:Set(mystack) end @@ -1963,7 +2097,7 @@ function AIRBOSS:_CheckCollapseMarshalStack(flight) -- TODO: Message to all players! MESSAGE:New(text, 15, "MARSHAL"):ToAll() - --self:MessageToAll(text, "MARSHAL", flight) + self:MessageToAll(text, "MARSHAL", flight) -- Hint for human players. if not flight.ai then @@ -2827,7 +2961,7 @@ function AIRBOSS:_Holding(playerData) local stack=playerData.flag:Get() -- Pattern alitude. - local patternalt, c1, c2=self:_GetMarshalAltitude(stack) + local patternalt, c1, c2=self:_GetMarshalAltitude(stack, playerData.case) -- Player altitude. local playeralt=unit:GetAltitude() @@ -2912,13 +3046,13 @@ function AIRBOSS:_Commencing(playerData) self:_InitPlayer(playerData) -- Commence - local text=string.format("Commencing. (Case %d)", self.case) + local text=string.format("Commencing. (Case %d)", playerData.case) -- Message to all players. self:MessageToAll(text, playerData.onboard, "", 5) -- Next step: depends on case recovery. - if self.case==1 then + if playerData.case==1 then -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. playerData.step=AIRBOSS.PatternStep.INITIAL else @@ -2936,8 +3070,8 @@ function AIRBOSS:_Initial(playerData) if playerData.unit:IsInZone(self.zoneInitial) then -- Inform player. - local hint=string.format("Entering the pattern.") - if playerData.difficulty==AIRBOSS.Difficulty.EASY then + local hint=string.format("Initial") + if playerData.difficulty==AIRBOSS.Difficulty.EASY then hint=hint.."\nAim for 800 feet and 350 kts at the break entry." end @@ -2966,6 +3100,12 @@ function AIRBOSS:_Platform(playerData) self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") playerData.warning=true end + + -- Back in zone. + if not invalid and playerData.warning then + self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "AIRBOSS") + playerData.warning=false + end -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZonePlatform(playerData.case)) @@ -3027,9 +3167,8 @@ function AIRBOSS:_ArcInTurn(playerData) end -- Back in zone. - -- TODO: add this to the other checkpoints! if not invalid and playerData.warning then - self:MessageToPlayer(playerData, "You are back in the approach corridor. Now stay there!", "AIRBOSS") + self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "AIRBOSS") playerData.warning=false end @@ -3054,8 +3193,7 @@ function AIRBOSS:_ArcInTurn(playerData) end -- Next step: Arc Out Turn. - playerData.step=AIRBOSS.PatternStep.ARCOUT - + playerData.step=AIRBOSS.PatternStep.ARCOUT playerData.warning=nil end end @@ -3076,6 +3214,13 @@ function AIRBOSS:_ArcOutTurn(playerData) self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") playerData.warning=true end + + -- Back in zone. + if not invalid and playerData.warning then + self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "AIRBOSS") + playerData.warning=false + end + -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZoneArcOut(playerData.case)) @@ -3129,6 +3274,13 @@ function AIRBOSS:_DirtyUp(playerData) self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") playerData.warning=true end + + -- Back in zone. + if not invalid and playerData.warning then + self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "AIRBOSS") + playerData.warning=false + end + -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) @@ -3156,8 +3308,7 @@ function AIRBOSS:_DirtyUp(playerData) end -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). - playerData.step=AIRBOSS.PatternStep.BULLSEYE - + playerData.step=AIRBOSS.PatternStep.BULLSEYE playerData.warning=nil end end @@ -3178,6 +3329,13 @@ function AIRBOSS:_Bullseye(playerData) self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") playerData.warning=true end + + -- Back in zone. + if not invalid and playerData.warning then + self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "AIRBOSS") + playerData.warning=false + end + -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) @@ -3205,8 +3363,7 @@ function AIRBOSS:_Bullseye(playerData) end -- Next step: Groove Call the ball. - playerData.step=AIRBOSS.PatternStep.GROOVE_XX - + playerData.step=AIRBOSS.PatternStep.GROOVE_XX playerData.warning=nil end end @@ -3249,6 +3406,7 @@ function AIRBOSS:_Upwind(playerData) -- Next step: Early Break. playerData.step=AIRBOSS.PatternStep.EARLYBREAK + playerData.warning=nil end end @@ -3298,6 +3456,7 @@ function AIRBOSS:_Break(playerData, part) else playerData.step=AIRBOSS.PatternStep.ABEAM end + playerData.warning=nil end end @@ -3327,6 +3486,7 @@ function AIRBOSS:_CheckForLongDownwind(playerData) -- Next step: Debriefing. playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil end end @@ -3377,6 +3537,7 @@ function AIRBOSS:_Abeam(playerData) -- Next step: ninety. playerData.step=AIRBOSS.PatternStep.NINETY + playerData.warning=nil end end @@ -3423,6 +3584,7 @@ function AIRBOSS:_Ninety(playerData) -- Next step: wake. playerData.step=AIRBOSS.PatternStep.WAKE + playerData.warning=nil elseif relheading>90 and self:_CheckLimits(X, Z, self.Wake) then -- Message to player. @@ -3471,6 +3633,7 @@ function AIRBOSS:_Wake(playerData) -- Next step: Final. playerData.step=AIRBOSS.PatternStep.FINAL + playerData.warning=nil end end @@ -3536,6 +3699,7 @@ function AIRBOSS:_Final(playerData) -- Next step: X start & call the ball. playerData.step=AIRBOSS.PatternStep.GROOVE_XX + playerData.warning=nil end end @@ -3598,6 +3762,7 @@ function AIRBOSS:_Groove(playerData) -- Next step: roger ball. playerData.step=AIRBOSS.PatternStep.GROOVE_RB + playerData.warning=nil elseif rho<=RRB and playerData.step==AIRBOSS.PatternStep.GROOVE_RB then @@ -3610,6 +3775,7 @@ function AIRBOSS:_Groove(playerData) -- Next step: in the middle. playerData.step=AIRBOSS.PatternStep.GROOVE_IM + playerData.warning=nil elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then @@ -3623,6 +3789,7 @@ function AIRBOSS:_Groove(playerData) -- Next step: in close. playerData.step=AIRBOSS.PatternStep.GROOVE_IC + playerData.warning=nil elseif rho<=RIC and playerData.step==AIRBOSS.PatternStep.GROOVE_IC then @@ -3654,6 +3821,7 @@ function AIRBOSS:_Groove(playerData) else -- Next step: AR at the ramp. playerData.step=AIRBOSS.PatternStep.GROOVE_AR + playerData.warning=nil end end @@ -3670,6 +3838,7 @@ function AIRBOSS:_Groove(playerData) -- Next step: in the wires. playerData.step=AIRBOSS.PatternStep.GROOVE_IW + playerData.warning=nil end -- Time since last LSO call. @@ -3700,7 +3869,7 @@ function AIRBOSS:_Groove(playerData) -- Next step: debrief. playerData.step=AIRBOSS.PatternStep.DEBRIEF - + playerData.warning=nil end end end @@ -3806,6 +3975,7 @@ function AIRBOSS:_Trapped(playerData, wire) -- Next step: debriefing. playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4039,6 +4209,7 @@ function AIRBOSS:_GetZoneCorridor(case) -- Width of the box in NM. local w=2 + -- Length of the box in NM. local l=10 @@ -4050,33 +4221,38 @@ function AIRBOSS:_GetZoneCorridor(case) -- Distance from ArcIn to ArcOut zone local y=d*math.tan(alpha) - - --local d2=math.cos(alpha)/2 + + -- Little extra bit along X. + local x=w/2*math.tan(alpha) -- Get the extra bit we need to go back from the end to the arc turn in. local C=w/math.cos(alpha) local b=w*math.tan(alpha) local a=C-b + local k=w/2/math.cos(alpha) self:I(string.format("FF w = %.1f NM", w)) self:I(string.format("FF l = %.1f NM", l)) self:I(string.format("FF d = %.1f NM", d)) self:I(string.format("FF y = %.1f NM", y)) - self:I(string.format("FF C = %.1f NM", C)) - self:I(string.format("FF b = %.1f NM", b)) - self:I(string.format("FF a = %.1f NM", a)) + --self:I(string.format("FF C = %.1f NM", C)) + --self:I(string.format("FF b = %.1f NM", b)) + --self:I(string.format("FF a = %.1f NM", a)) + self:I(string.format("FF x = %.1f NM", y)) + self:I(string.format("FF k = %.1f NM", k)) -- TODO: Still not right! + -- TODO: Does this still work with alpha=0?e local c={} c[1]=self:GetCoordinate() -- Carrier coordinate c[2]=c[1]:Translate( UTILS.NMToMeters(w/2), radial-90) -- 1 Right of carrier c[3]=c[2]:Translate( UTILS.NMToMeters(d+w/2), radial) -- 13 "south" @ 1 right - c[4]=c[3]:Translate( UTILS.NMToMeters(y), radial+90) -- y left @ 13 south + c[4]=c[3]:Translate( UTILS.NMToMeters(y+x), radial+90) -- y+x left @ 13 south c[5]=c[4]:Translate( UTILS.NMToMeters(l), offset) -- 10 NM to back wall (angled) c[6]=c[5]:Translate( UTILS.NMToMeters(w), offset+90) -- Back wall (angled) - c[7]=c[6]:Translate(-UTILS.NMToMeters(l+a), offset) -- 10+a Back along X & Z - c[8]=c[7]:Translate( UTILS.NMToMeters(y), radial-90) -- Back along X + c[7]=c[6]:Translate(-UTILS.NMToMeters(l+k), offset) -- 10+a Back along X & Z + c[8]=c[7]:Translate( UTILS.NMToMeters(y-x), radial-90) -- y-x back along X c[9]=c[1]:Translate( UTILS.NMToMeters(w/2), radial+90) -- 1 left of carrier -- Create an array of a square! @@ -4090,7 +4266,7 @@ function AIRBOSS:_GetZoneCorridor(case) -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. -- So stay 0-5 NM (+1 NM error margin) port of carrier. - local zone=ZONE_POLYGON_BASE:New("CASE II/III Valid Zone", p) + local zone=ZONE_POLYGON_BASE:New("CASE II/III Approach Corridor", p) return zone end @@ -4910,6 +5086,7 @@ function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) -- Next step debrief. playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil end -- Message to player. @@ -4973,12 +5150,12 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) elseif _error<-lowscore then hint=string.format("You're slightly low.") else - hint=string.format("Good altitude. ") + hint=string.format("Good altitude.") end -- Extend or decrease depending on skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then - hint=hint..string.format("Optimal altitude is %d ft.", UTILS.MetersToFeet(altopt)) + hint=hint..string.format(" Optimal altitude is %d ft.", UTILS.MetersToFeet(altopt)) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then --hint=hint.."\n" elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then @@ -4986,7 +5163,7 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) end -- Debrief text. - local debrief=string.format("Altitude %d ft = %d%% deviation from %d ft optimum.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(altopt)) + local debrief=string.format("Altitude %d ft = %d%% deviation from %d ft.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(altopt)) return hint, debrief end @@ -5035,7 +5212,7 @@ function AIRBOSS:_DistanceCheck(playerData, optdist) end -- Debriefing text. - local debrief=string.format("Distance %.1f NM = %d%% deviation from %.1f NM optimum.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(optdist)) + local debrief=string.format("Distance %.1f NM = %d%% deviation from %.1f NM.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(optdist)) return hint, debrief end @@ -5084,7 +5261,7 @@ function AIRBOSS:_AoACheck(playerData, optaoa) end -- Debriefing text. - local debrief=string.format("AoA %.1f = %d%% deviation from %.1f optimum.", aoa, _error, optaoa) + local debrief=string.format("AoA %.1f = %d%% deviation from %.1f.", aoa, _error, optaoa) return hint, debrief end @@ -5210,6 +5387,7 @@ function AIRBOSS:_Debrief(playerData) -- Commencing again. playerData.step=AIRBOSS.PatternStep.COMMENCING + playerData.warning=nil else -- Unit does not seem to be alive! @@ -5232,6 +5410,7 @@ function AIRBOSS:_Debrief(playerData) -- Next step. playerData.step=AIRBOSS.PatternStep.UNDEFINED + playerData.warning=nil end -- Increase number of passes. @@ -5457,6 +5636,40 @@ function AIRBOSS:GetCoordinate() return self.carrier:GetCoordinate() end + +--- Get mission weather. +-- @param #AIRBOSS self +function AIRBOSS:_MissionWeather() + + -- Weather data from mission file. + local weather=env.mission.weather + + + --[[ + ["clouds"] = + { + ["thickness"] = 430, + ["density"] = 7, + ["base"] = 0, + ["iprecptns"] = 1, + }, -- end of ["clouds"] + ]] + local clouds=weather.clouds + + --[[ + ["fog"] = + { + ["thickness"] = 0, + ["visibility"] = 25, + }, -- end of ["fog"] + ]] + local fog=weather.fog + + -- Visibilty distance in meters. + local vis=weather.visibility.distance + +end + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- RADIO MESSAGE Functions ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5654,8 +5867,15 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration self:I(self.lid..text) -- TODO: Test! Need to make this better!. + -- TODO: This will fail with message to all since for each player the message will be played! if receiver==playerData.onboard then - self:_Number2Sound(self.LSOradio, receiver, delay) + if sender then + if sender=="LSO" then + self:_Number2Sound(self.LSOradio, receiver, delay) + elseif sender=="MARSHALL" then + self:_Number2Sound(self.Carrierradio, receiver, delay) + end + end end if delay and delay>0 then @@ -5818,8 +6038,8 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _helpPath, self._AttitudeMonitor, self, playername) missionCommands.addCommandForGroup(_gid, "Smoke Marshal Zone", _helpPath, self._MarkMarshalZone, self, _unitName, false) missionCommands.addCommandForGroup(_gid, "Flare Marshal Zone", _helpPath, self._MarkMarshalZone, self, _unitName, true) - missionCommands.addCommandForGroup(_gid, "Smoke R. Case Zones", _helpPath, self._MarkCase23Zones, self, _unitName, false) - missionCommands.addCommandForGroup(_gid, "Flare R. Case Zones", _helpPath, self._MarkCase23Zones, self, _unitName, true) + missionCommands.addCommandForGroup(_gid, "Smoke Pattern Zones", _helpPath, self._MarkCase23Zones, self, _unitName, false) + missionCommands.addCommandForGroup(_gid, "Flare Pattern Zones", _helpPath, self._MarkCase23Zones, self, _unitName, true) missionCommands.addCommandForGroup(_gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) -- F10/Airboss//Kneeboard @@ -5904,20 +6124,20 @@ function AIRBOSS:_RequestMarshal(_unitName) if self:_InQueue(self.Qmarshal, playerData.group) then -- Flight group is already in marhal queue. - local text=string.format("%s, you are already in the Marshal queue. New marshal request denied!", playerData.name) - MESSAGE:New(text, 10, "MARSHAL"):ToClient(playerData.client) + local text=string.format("you are already in the Marshal queue. New marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") elseif self:_InQueue(self.Qpattern, playerData.group) then -- Flight group is already in pattern queue. - local text=string.format("%s, you are already in the Pattern queue. Marshal request denied!", playerData.name) - MESSAGE:New(text, 10, "MARSHAL"):ToClient(playerData.client) + local text=string.format("you are already in the Pattern queue. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") elseif not _unit:InAir() then -- Flight group is already in pattern queue. - local text=string.format("%s, you are not airborn. Marshal request denied!", playerData.name) - MESSAGE:New(text, 10, "MARSHAL"):ToClient(playerData.client) + local text=string.format("you are not airborn. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") else @@ -5929,8 +6149,8 @@ function AIRBOSS:_RequestMarshal(_unitName) else -- Flight group is not in CCA yet. - local text=string.format("%s, you are not inside CCA yet. Marshal request denied!", playerData.name) - MESSAGE:New(text, 10, "MARSHAL"):ToClient(playerData.client) + local text=string.format("you are not inside CCA yet. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") end end @@ -5999,6 +6219,7 @@ function AIRBOSS:_RequestCommence(_unitName) -- Set player step. playerData.step=AIRBOSS.PatternStep.COMMENCING + playerData.warning=nil -- Collaps marshal stack. self:_CollapseMarshalStack(playerData, false) @@ -6051,11 +6272,14 @@ function AIRBOSS:_RequestRefueling(_unitName) -- Tanker is up and running. text=string.format("Proceed to tanker at angels %d.", angels) - --TODO: State TACAN channel of tanker if defined. + -- State TACAN channel of tanker if defined. + if self.tanker.TACANon then + text=text..string.format("\nTanker TACAN channel %d%s (%s)", self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) + end -- Tanker is currently refueling. Inform player. if self.tanker:IsRefueling() then - text=text.."\n Tanker is currently refueling. You might have to queue up." + text=text.."\nTanker is currently refueling. You might have to queue up." end -- Collapse marshal stack if player is in queue. @@ -6345,14 +6569,13 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocityMPS()) -- Tacan/ICLS. - -- TODO: adjust to option TACAN or ICLS disabled. local tacan="unknown" local icls="unknown" - if self.TACANchannel~=nil then - tacan=string.format("%d%s", self.TACANchannel, self.TACANmode) + if self.TACANon and self.TACANchannel~=nil then + tacan=string.format("%d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse) end - if self.ICLSchannel~=nil then - icls=string.format("%d", self.ICLSchannel) + if self.ICLSon and self.ICLSchannel~=nil then + icls=string.format("%d (%s)", self.ICLSchannel, self.ICLSmorse) end -- Get groups, units in queues. diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index f3dd49e54..44b615e3d 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -46,6 +46,7 @@ -- @field #boolean uncontrolledac If true, use and uncontrolled tanker group already present in the mission. -- @field DCS#Vec3 orientation Orientation of the carrier. Used to monitor changes and update the pattern if heading changes significantly. -- @field Core.Point#COORDINATE position Positon of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. +-- @field Core.Zone#ZONE_UNIT zoneUpdate Moving zone relative to carrier. Each time the tanker is in this zone, its pattern is updated. -- @extends Core.Fsm#FSM --- Recovery Tanker. @@ -204,11 +205,12 @@ RECOVERYTANKER = { uncontrolledac = nil, orientation = nil, position = nil, + zoneUpdate = nil, } --- Class version. -- @field #string version -RECOVERYTANKER.version="0.9.4w" +RECOVERYTANKER.version="0.9.5w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -268,6 +270,9 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetPatternUpdateDistance() self:SetPatternUpdateHeading() self:SetPatternUpdateInterval() + + -- Moving zone: Zone 1 NM astern the carrier with radius of 1.0 km. + self.zoneUpdate=ZONE_UNIT:New("Pattern Update Zone", self.carrier, 1*1000, {dx=-UTILS.NMToMeters(1), dy=0, relative_to_unit=true}) ----------------------- --- FSM Transitions --- @@ -707,6 +712,15 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) local text=string.format("Recovery tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) self:T(text) + -- Check if tanker flies through pattern update zone. + -- TODO: Check if this can be used to update the pattern without too much disruption. + -- Could be a problem when carrier changes course since the tanker might not fligh through the zone any more. + local inupdatezone=self.tanker:GetUnit(1):IsInZone(self.zoneUpdate) + if inupdatezone then + local clock=UTILS.SecondsToClock(timer.getAbsTime()) + self:I(string.format("Recovery tanker is in pattern update zone! Time=%s", clock)) + end + -- Check if tanker is running and not RTBing or refueling. if self:IsRunning() then @@ -763,9 +777,9 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) end - -- Call status again in 1 minute. + -- Call status again in 30 seconds. if not self:IsStopped() then - self:__Status(-60) + self:__Status(-30) end end @@ -910,10 +924,10 @@ function RECOVERYTANKER:_RefuelingStart(EventData) if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() then -- Unit receiving fuel. - local unit=EventData.IniUnit + local receiver=EventData.IniUnit -- Get distance to tanker to check that unit is receiving fuel from this tanker. - local dist=unit:GetCoordinate():Get2DDistance(self.tanker:GetCoordinate()) + local dist=receiver:GetCoordinate():Get2DDistance(self.tanker:GetCoordinate()) -- If distance > 100 meters, this should be another tanker. if dist>100 then @@ -921,7 +935,7 @@ function RECOVERYTANKER:_RefuelingStart(EventData) end -- Info message. - self:T(string.format("Recovery tanker %s started refueling unit %s", self.tanker:GetName(), unit:GetName())) + self:T(string.format("Recovery tanker %s started refueling unit %s", self.tanker:GetName(), receiver:GetName())) -- FMS state "Refueling". self:RefuelStart(receiver) @@ -938,10 +952,10 @@ function RECOVERYTANKER:_RefuelingStop(EventData) if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() then -- Unit receiving fuel. - local unit=EventData.IniUnit + local receiver=EventData.IniUnit -- Get distance to tanker to check that unit is receiving fuel from this tanker. - local dist=unit:GetCoordinate():Get2DDistance(self.tanker:GetCoordinate()) + local dist=receiver:GetCoordinate():Get2DDistance(self.tanker:GetCoordinate()) -- If distance > 100 meters, this should be another tanker. if dist>100 then @@ -949,10 +963,10 @@ function RECOVERYTANKER:_RefuelingStop(EventData) end -- Info message. - self:T(string.format("Recovery tanker %s stopped refueling unit %s", self.tanker:GetName(), unit:GetName())) + self:T(string.format("Recovery tanker %s stopped refueling unit %s", self.tanker:GetName(), receiver:GetName())) -- FSM state "Running". - self:RefuelStop(unit) + self:RefuelStop(receiver) end end @@ -1116,6 +1130,52 @@ function RECOVERYTANKER:_ActivateTACAN(delay) end +--- Calculate distances between carrier and tanker. +-- @param #AIRBOSS self +-- @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 RECOVERYTANKER:_GetDistances() + + -- Vector to carrier + local a=self.carrier:GetVec3() + + -- Vector to player + local b=self.tanker: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 + + if phi<0 then + phi=phi+360 + end + + return dx,dz,rho,phi +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- From 047df5917a913b3c7c67058077dd83f069bda6c1 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 4 Dec 2018 16:05:15 +0100 Subject: [PATCH 075/147] AIRBOSS v0.4.4w --- Moose Development/Moose/Ops/Airboss.lua | 187 ++++++++++++++++-------- 1 file changed, 125 insertions(+), 62 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 7fd89c4f6..0d952a0b5 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -191,7 +191,7 @@ AIRBOSS.AircraftPlayer={ -- @type AIRBOSS.AircraftCarrier -- @field #string AV8B AV-8B Night Harrier. -- @field #string HORNET F/A-18C Lot 20 Hornet. --- @field #string A4EC Community A-4E-C mod. +-- @field #string A4EC Community A-4E mod. -- @field #string S3B Lockheed S-3B Viking. -- @field #string S3BTANKER Lockheed S-3B Viking tanker. -- @field #string E2D Grumman E-2D Hawkeye AWACS. @@ -288,7 +288,7 @@ AIRBOSS.PatternStep={ -- @field #AIRBOSS.RadioSound SLOW "You're slow" call. -- @field #AIRBOSS.RadioSound PADDLESCONTACT "Paddles, contact" call. -- @field #AIRBOSS.RadioSound CALLTHEBALL "Call the Ball" --- @field #AIRBOSS.RadioSound ROGERBALL "Roger ball" call (actually from pilot). +-- @field #AIRBOSS.RadioSound ROGERBALL "Roger ball" call. -- @field #AIRBOSS.RadioSound WAVEOFF "Wafe off" call -- @field #AIRBOSS.RadioSound BOLTER "Bolter, Bolter" call -- @field #AIRBOSS.RadioSound LONGINGROOVE "You're long in the groove. Depart and re-enter." call. @@ -684,12 +684,13 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.3w" +AIRBOSS.version="0.4.4w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Update AI holding pattern wrt to moving carrier. -- TODO: Extract (static) weather from mission for cloud covery etc. -- TODO: Option to filter AI groups for recovery. -- TODO: Option to turn AI handling off. @@ -828,7 +829,7 @@ function AIRBOSS:New(carriername, alias) self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) - self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) end --[[ @@ -1545,7 +1546,7 @@ function AIRBOSS:_GetAircraftAoA(playerData) aoa.OnSpeedMin=7.4 aoa.Fast=6.9 elseif skyhawk then - -- A-4-E parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 + -- A-4E-C parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 aoa.Slow=18.5 aoa.OnSpeedMax=18.0 aoa.OnSpeed=17.5 @@ -1848,7 +1849,7 @@ function AIRBOSS:_ScanCarrierZone() self:_MarshalAI(knownflight, stack) -- Add group to marshal stack queue. - self:_AddMarshallGroup(knownflight, stack) + self:_AddMarshalGroup(knownflight, stack) end end @@ -1893,7 +1894,7 @@ function AIRBOSS:_MarshalPlayer(playerData) local mystack=ngroups+1 -- Add group to marshal stack. - self:_AddMarshallGroup(playerData, mystack) + self:_AddMarshalGroup(playerData, mystack) -- Set step to holding. playerData.step=AIRBOSS.PatternStep.HOLDING @@ -2057,7 +2058,7 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.Flightitem flight Flight group. -- @param #number stack Marshal stack. This (re-)sets the flag value. -function AIRBOSS:_AddMarshallGroup(flight, stack) +function AIRBOSS:_AddMarshalGroup(flight, stack) -- Set flag value. This corresponds to the stack number which starts at 1. flight.flag:Set(stack) @@ -2090,29 +2091,26 @@ end -- @param #AIRBOSS.Flightitem flight Flight to go to pattern. function AIRBOSS:_CheckCollapseMarshalStack(flight) - -- TODO: better message. - local text=string.format("%s, you are cleared for Case %d recovery pattern!", flight.groupname, flight.case) - -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence. if flight.ai then -- Collapse stack and send AI to pattern. self:_CollapseMarshalStack(flight) - else - -- TODO only if skil is not TOPGUN - --text=text.. end - - -- TODO: Message to all players! - MESSAGE:New(text, 15, "MARSHAL"):ToAll() - self:MessageToAll(text, "MARSHAL", flight) + + -- Inform all flights. + local text=string.format("You are cleared for Case %d recovery.", flight.case) + self:MessageToAll(text, "MARSHAL", flight.onboard) -- Hint for human players. if not flight.ai then local playerData=flight --#AIRBOSS.PlayerData - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - self:MessageToPlayer(flight, string.format("\nUse F10 radio menu \"Commence!\" command when you are ready!"), nil, "", 5) + + -- Hint for easy skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + self:MessageToPlayer(flight, string.format("Use F10 radio menu \"Commence!\" command when you are ready!"), nil, "", 5) end end + end --- Collapse marshal stack. @@ -2130,7 +2128,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- Decrease flag values of all flight groups in marshal stack. for _,_flight in pairs(self.Qmarshal) do - local mflight=_flight --#AIRBOSS.Flightitem + local mflight=_flight --#AIRBOSS.PlayerData -- Only collaps stack of which the flight left. CASE II/III stack is the same. if (case==1 and mflight.case==1) or (case>1 and mflight.case>1) then @@ -2139,7 +2137,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) local mstack=mflight.flag:Get() -- Only collapse stacks above the new pattern flight. - -- TODO: this will go wrong, if patternflight is not in marshal stack because it will have value -100 and all mstacks will be larger! + -- This will go wrong, if patternflight is not in marshal stack because it will have value -100 and all mstacks will be larger! -- Maybe need to set the initial value to 1000? Or check pstack>0? if stack>0 and mstack>stack then @@ -2147,16 +2145,23 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) mflight.flag:Set(mstack-1) -- Inform players. - if mflight.player then + if mflight.player and mflight.difficulty~=AIRBOSS.Difficulty.HARD then local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(mstack-1,case)) local text=string.format("descent to next lower stack at %d ft", alt) - self:MessageToPlayer(mflight, text, "MARSHAL", nil, 10) + self:MessageToPlayer(mflight, text, "MARSHAL") end -- Also decrease flag for section members of flight. for _,_sec in pairs(mflight.section) do local sec=_sec --#AIRBOSS.PlayerData sec.flag:Set(mstack-1) + + -- Inform section member. + if sec.difficulty~=AIRBOSS.Difficulty.HARD then + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(mstack-1,case)) + local text=string.format("follow your lead to next lower stack at %d ft", alt) + self:MessageToPlayer(sec, text, "MARSHAL") + end end end @@ -2268,14 +2273,42 @@ function AIRBOSS:_PrintQueue(queue, name) else for i,_flight in pairs(queue) do local flight=_flight --#AIRBOSS.Flightitem + + -- Timestamp. local clock=UTILS.SecondsToClock(flight.time) - local stack=flight.flag:Get() - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) - local fuel=flight.group:GetFuelMin()*100 + -- Recovery case of flight. local case=flight.case + -- Stack and stack alt. + local stack=flight.flag:Get() + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, case)) + -- Fuel %. + local fuel=flight.group:GetFuelMin()*100 + --local fuelstate=self:_GetFuelState(unit) local ai=tostring(flight.ai) - text=text..string.format("\n[%d] %s*%d: stackalt=%d ft, flag=%d, case=%d, time=%s, fuel=%d, ai=%s", - i, flight.groupname, flight.nunits, alt, stack, case, clock, fuel, ai) + --flight.onboard + local lead=flight.seclead + local nsec=#flight.section + local actype=flight.actype + local onboard=flight.onboard + -- TODO: Include player data. + --[[ + if not flight.ai then + local playerData=_flight --#AIRBOSS.PlayerData + e=playerData.name + c=playerData.difficulty + f=playerData.passes + g=playerData.step + j=playerData.warning + a=playerData.holding + b=playerData.landed + d=playerData.boltered + h=playerData.lig + i=playerData.patternwo + k=playerData.waveoff + end + ]] + text=text..string.format("\n[%d] %s*%d (%s): lead=%s (%d), onboard=%s, stackalt=%d ft, flag=%d, case=%d, time=%s, fuel=%d, ai=%s", + i, flight.groupname, flight.nunits, actype, lead, nsec, onboard, alt, stack, case, clock, fuel, ai) end end self:I(self.lid..text) @@ -2777,14 +2810,9 @@ function AIRBOSS:OnEventBirth(EventData) -- Init player data. self.players[_playername]=self:_NewPlayer(_unitName) - --env.info("FF radiocall LSO long in groove") - --self:RadioTransmission(self.LSOradio, self.radiocall["LONGINGROOVE"], false, 5) - --self:RadioTransmission(self.LSOradio, self.radiocall.LONGINGROOVE, false, 20) - - - self:_Number2Sound(self.LSOradio, "129", 10) - + -- Debug. if self.Debug then + self:_Number2Sound(self.LSOradio, "0123456789", 10) --self:_MarkCase23Zones(_unit:GetName()) end @@ -3018,10 +3046,17 @@ function AIRBOSS:_Holding(playerData) -- Player did not entered the holding zone yet. if inholdingzone then + -- Player arrived in holding zone. playerData.holding=true + + -- Debug output. self:I("Player entered the holding zone for the first time.") + + -- Inform player. text=text..string.format("You arrived at the holding zone.") + + -- Feedback on altitude. if goodalt then text=text..string.format(" Now stay at that altitude.") else @@ -3040,7 +3075,7 @@ function AIRBOSS:_Holding(playerData) end -- Send message. - self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 5) + self:MessageToPlayer(playerData, text, "MARSHAL", nil, 5) end @@ -3259,8 +3294,7 @@ function AIRBOSS:_ArcOutTurn(playerData) playerData.step=AIRBOSS.PatternStep.DIRTYUP else -- ERROR! - end - + end playerData.warning=nil end end @@ -3763,7 +3797,12 @@ function AIRBOSS:_Groove(playerData) -- LSO "Call the ball" call. self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.CALLTHEBALL) playerData.Tlso=timer.getTime() - + + -- Pilot "405, Hornet Ball, 3.2" + -- TODO: Pilot output should come from pilot in MP. + local text=string.format("Hornet Ball, %.1f", self:_GetFuelState(playerData.unit)) + self:MessageToPlayer(playerData, text, playerData.onboard, "", 3, false, 3) + -- Store data. playerData.groove.XX=groovedata @@ -3773,7 +3812,7 @@ function AIRBOSS:_Groove(playerData) elseif rho<=RRB and playerData.step==AIRBOSS.PatternStep.GROOVE_RB then - -- Pilot: "Roger ball" call. + -- LSO "Roger ball" call. self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.ROGERBALL) playerData.Tlso=timer.getTime()+1 @@ -3787,7 +3826,7 @@ function AIRBOSS:_Groove(playerData) elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then -- Debug. - local text=string.format("FF IM=%d", rho) + local text=string.format("Groove IM=%d m", rho) MESSAGE:New(text, 5):ToAllIf(self.Debug) self:I(self.lid..string.format("FF IM=%d", rho)) @@ -3804,7 +3843,7 @@ function AIRBOSS:_Groove(playerData) if playerData.waveoff==false then -- Debug - local text=string.format("FF IC=%d", rho) + local text=string.format("Groove IC=%d m", rho) MESSAGE:New(text, 5):ToAllIf(self.Debug) self:I(self.lid..text) @@ -3836,7 +3875,7 @@ function AIRBOSS:_Groove(playerData) elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AR then -- Debug. - local text=string.format("FF AR=%d", rho) + local text=string.format("Groove AR=%d m", rho) MESSAGE:New(text, 5):ToAllIf(self.Debug) self:I(self.lid..text) @@ -3910,6 +3949,7 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, difficulty) end -- Too slow or too fast? + --TODO: Get aircraft dependent values. Needs playerData! if AoA<6.9 or AoA>9.3 then if difficulty==AIRBOSS.Difficulty.HARD then self:I(self.lid.."Wave off due to AoA<6.9 or AoA>9.3!") @@ -5551,6 +5591,19 @@ function AIRBOSS:_GetFuelState(unit) return UTILS.kg2lbs(fuelstate) end +--- Get altitude in angels. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. +-- @return #number Altitude of unit in Anglels = thouthands of feet. +function AIRBOSS:_GetAngels(unit) + + local alt=unit:GetAltitude() + + local angels=math.floor(UTILS.MetersToFeet(alt))/1000 + + return angels +end + --- Get unit masses especially fuel from DCS descriptor values. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. @@ -5870,7 +5923,8 @@ end -- @param #number duration Display message duration. Default 10 seconds. -- @param #boolean clear If true, clear screen from previous messages. -- @param #number delay Delay in seconds, before the message is displayed. -function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay) +-- @param #boolean soundoff If true, do not play boad number message. +function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) if playerData and message and message~="" then @@ -5882,18 +5936,19 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration if receiver and receiver=="" then text=string.format("%s", message) else + -- Default "receiver" is onboard number of player. receiver=receiver or playerData.onboard text=string.format("%s, %s", receiver, message) end self:I(self.lid..text) -- TODO: Test! Need to make this better!. - -- TODO: This will fail with message to all since for each player the message will be played! - if receiver==playerData.onboard then + -- DONE: This will fail with message to all since for each player the message will be played! + if receiver==playerData.onboard and not soundoff then if sender then - if sender=="LSO" then + if sender=="LSO" or sender =="AIRBOSS" then self:_Number2Sound(self.LSOradio, receiver, delay) - elseif sender=="MARSHALL" then + elseif sender=="MARSHAL" then self:_Number2Sound(self.Carrierradio, receiver, delay) end end @@ -5924,9 +5979,17 @@ function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay) for _,_player in pairs(self.players) do local player=_player --#AIRBOSS.PlayerData + + -- Message to all players in CCA. + -- TODO: could make something to all in pattern or all in marshal queue depending on sender. if player.unit:IsInZone(self.zoneCCA) then - self:MessageToPlayer(player, message, sender, receiver, duration, clear, delay) - end + + -- No sound here. + -- TODO: Need to improve? depending on sender. + self:MessageToPlayer(player, message, sender, receiver, duration, clear, delay, true) + + end + end end @@ -6269,13 +6332,13 @@ function AIRBOSS:_RequestCommence(_unitName) -- Check if pattern is already full. if npattern>self.Nmaxpattern then -- Patern is full! - text=string.format("Negative ghostrider, pattern is full! There are %d aircraft currently in pattern.", npattern) + text=string.format("Negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern) else -- Positive response. if playerData.case==1 then - text="You are cleared for pattern. Proceed to initial." + text="Proceed to initial." else - text="You are cleared for pattern. Descent at 4k ft/min to platform at 5000 ft." + text="Descent at 4k ft/min to platform at 5000 ft." end -- Set player step. @@ -6291,14 +6354,15 @@ function AIRBOSS:_RequestCommence(_unitName) end else -- This flight is not yet registered! - text="Negative ghostrider, you are not yet registered inside the CCA yet!" + text="Negative ghostrider, you are not inside the CCA yet!" -- TODO: fly 10 km towards the carrier advice for skill "Flight Student" end - env.info(text) + -- Debug + self:I(self.lid..text) -- Send message. - self:MessageToPlayer(playerData, text, "AIRBOSS", "", 5) + self:MessageToPlayer(playerData, text, "MARSHAL") end end end @@ -6361,7 +6425,7 @@ function AIRBOSS:_RequestRefueling(_unitName) end -- Send message. - self:MessageToPlayer(playerData, text, "AIRBOSS") + self:MessageToPlayer(playerData, text, "MARSHAL") end end end @@ -6385,8 +6449,6 @@ function AIRBOSS:_SetSection(_unitName) -- TODO: Only allow set section, if player is not in marshal stack yet. - local text - -- Loop over all registered flights. for _,_flight in pairs(self.flights) do local flight=_flight --#AIRBOSS.Flightitem @@ -6405,6 +6467,7 @@ function AIRBOSS:_SetSection(_unitName) end -- Info on section members. + local text if #playerData.section>0 then text=string.format("Registered flight section:") text=text..string.format("- %s (lead)", playerData.name) @@ -6414,14 +6477,14 @@ function AIRBOSS:_SetSection(_unitName) flight.seclead=playerData.name -- Inform player that he is now part of a section. - self:MessageToPlayer(flight, string.format("Your section lead is now %s", playerData.name), self.carrier:GetName(), "", 10) + self:MessageToPlayer(flight, string.format("Your section lead is now %s.", playerData.name), "MARSHAL") end else text="No other human flights found within radius of 200 meters!" end -- Message to section lead. - self:MessageToPlayer(playerData, text, self.alias, "", 10) + self:MessageToPlayer(playerData, text, "MARSHAL") end end From a83008aad33b5ce91b8047d603e01b0f6f377d73 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 4 Dec 2018 23:37:56 +0100 Subject: [PATCH 076/147] AIRBOSS v0.4.5 --- Moose Development/Moose/Core/Point.lua | 2 +- Moose Development/Moose/Ops/Airboss.lua | 250 ++++++++++-------- .../Moose/Ops/RecoveryTanker.lua | 63 ++++- 3 files changed, 189 insertions(+), 126 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index b79174989..366ad03e1 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -460,7 +460,7 @@ do -- COORDINATE -- @param #COORDINATE self -- @param DCS#Distance Distance The Distance to be added in meters. -- @param DCS#Angle Angle The Angle in degrees. Defaults to 0 if not specified (nil). - -- @return #COORDINATE The new calculated COORDINATE. + -- @return Core.Point#COORDINATE The new calculated COORDINATE. function COORDINATE:Translate( Distance, Angle ) local SX = self.x local SY = self.z diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 0d952a0b5..91d45c96d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -125,8 +125,8 @@ -- @field #AIRBOSS AIRBOSS = { ClassName = "AIRBOSS", + Debug = false, lid = nil, - Debug = true, carrier = nil, carriertype = nil, carrierparam = {}, @@ -407,76 +407,76 @@ AIRBOSS.LSOCall={ subtitle="Paddles, radio check", duration=1.0, }, + N0={ + file="LSO-N0", + suffix="ogg", + louder=false, + subtitle="0", + duration=0.5, + }, N1={ file="LSO-N1", suffix="ogg", louder=false, - subtitle="", + subtitle="1", duration=0.3, }, N2={ file="LSO-N2", suffix="ogg", louder=false, - subtitle="", + subtitle="2", duration=0.3, }, N3={ file="LSO-N3", suffix="ogg", louder=false, - subtitle="", + subtitle="3", duration=0.4, }, N4={ file="LSO-N4", suffix="ogg", louder=false, - subtitle="", + subtitle="4", duration=0.4, }, N5={ file="LSO-N5", suffix="ogg", louder=false, - subtitle="", + subtitle="5", duration=0.4, }, N6={ file="LSO-N6", suffix="ogg", louder=false, - subtitle="", + subtitle="6", duration=0.6, }, N7={ file="LSO-N7", suffix="ogg", louder=false, - subtitle="", + subtitle="7", duration=0.6, }, N8={ file="LSO-N8", suffix="ogg", louder=false, - subtitle="", + subtitle="8", duration=0.4, }, N9={ file="LSO-N9", suffix="ogg", louder=false, - subtitle="", + subtitle="9", duration=0.5, }, - N0={ - file="LSO-N0", - suffix="ogg", - louder=false, - subtitle="", - duration=0.4, - }, } --- Marshal radio calls. @@ -492,76 +492,76 @@ AIRBOSS.LSOCall={ -- @field #AIRBOSS.RadioSound N9 "Nine" call. -- @field #AIRBOSS.RadioSound N0 "Zero" call. AIRBOSS.MarshalCall={ + N0={ + file="LSO-N0", + suffix="ogg", + louder=false, + subtitle="0", + duration=0.5, + }, N1={ file="LSO-N1", suffix="ogg", louder=false, - subtitle="", + subtitle="1", duration=0.3, }, N2={ file="LSO-N2", suffix="ogg", louder=false, - subtitle="", + subtitle="2", duration=0.3, }, N3={ file="LSO-N3", suffix="ogg", louder=false, - subtitle="", + subtitle="3", duration=0.4, }, N4={ file="LSO-N4", suffix="ogg", louder=false, - subtitle="", + subtitle="4", duration=0.4, }, N5={ file="LSO-N5", suffix="ogg", louder=false, - subtitle="", + subtitle="5", duration=0.4, }, N6={ file="LSO-N6", suffix="ogg", louder=false, - subtitle="", + subtitle="6", duration=0.6, }, N7={ file="LSO-N7", suffix="ogg", louder=false, - subtitle="", + subtitle="7", duration=0.6, }, N8={ file="LSO-N8", suffix="ogg", louder=false, - subtitle="", + subtitle="8", duration=0.4, }, N9={ file="LSO-N9", suffix="ogg", louder=false, - subtitle="", + subtitle="9", duration=0.5, }, - N0={ - file="LSO-N0", - suffix="ogg", - louder=false, - subtitle="", - duration=0.4, - }, } --- Difficulty level. @@ -684,7 +684,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.4w" +AIRBOSS.version="0.4.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -769,7 +769,7 @@ function AIRBOSS:New(carriername, alias) -- Set up Airboss radio. self.Carrierradio=RADIO:New(self.carrier) - self.Carrierradio:SetAlias("AIRBOSS") + self.Carrierradio:SetAlias("MARSHAL") self:SetCarrierradio() -- Set up LSO radio. @@ -1916,6 +1916,7 @@ function AIRBOSS:_MarshalPlayer(playerData) -- Flight is not registered yet. local text="you are not yet registered inside the CCA. Marshal request denied!" self:MessageToPlayer(playerData, text, "MARSHAL") + end end @@ -2079,7 +2080,7 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) text=text..string.format("Altimeter %.2f. Report see me.", P) -- Message to all players. - self:MessageToAll(text, "MARSHAL") + self:MessageToAll(text, "MARSHAL", flight.onboard) -- Add to marshal queue. table.insert(self.Qmarshal, flight) @@ -2812,7 +2813,8 @@ function AIRBOSS:OnEventBirth(EventData) -- Debug. if self.Debug then - self:_Number2Sound(self.LSOradio, "0123456789", 10) +-- self:_Number2Sound(self.LSOradio, "0123456789", 10) + self:_Number2Sound(self.Carrierradio, "0123456789", 10) --self:_MarkCase23Zones(_unit:GetName()) end @@ -4283,24 +4285,25 @@ function AIRBOSS:_GetZoneCorridor(case) self:I(string.format("FF l = %.1f NM", l)) self:I(string.format("FF d = %.1f NM", d)) self:I(string.format("FF y = %.1f NM", y)) - --self:I(string.format("FF C = %.1f NM", C)) - --self:I(string.format("FF b = %.1f NM", b)) - --self:I(string.format("FF a = %.1f NM", a)) - self:I(string.format("FF x = %.1f NM", y)) + self:I(string.format("FF C = %.1f NM", C)) + self:I(string.format("FF b = %.1f NM", b)) + self:I(string.format("FF a = %.1f NM", a)) + self:I(string.format("FF x = %.1f NM", x)) self:I(string.format("FF k = %.1f NM", k)) -- TODO: Still not right! -- TODO: Does this still work with alpha=0?e local c={} c[1]=self:GetCoordinate() -- Carrier coordinate - c[2]=c[1]:Translate( UTILS.NMToMeters(w/2), radial-90) -- 1 Right of carrier - c[3]=c[2]:Translate( UTILS.NMToMeters(d+w/2), radial) -- 13 "south" @ 1 right - c[4]=c[3]:Translate( UTILS.NMToMeters(y+x), radial+90) -- y+x left @ 13 south - c[5]=c[4]:Translate( UTILS.NMToMeters(l), offset) -- 10 NM to back wall (angled) - c[6]=c[5]:Translate( UTILS.NMToMeters(w), offset+90) -- Back wall (angled) - c[7]=c[6]:Translate(-UTILS.NMToMeters(l+k), offset) -- 10+a Back along X & Z - c[8]=c[7]:Translate( UTILS.NMToMeters(y-x), radial-90) -- y-x back along X - c[9]=c[1]:Translate( UTILS.NMToMeters(w/2), radial+90) -- 1 left of carrier + c[2]=c[1]:Translate( UTILS.NMToMeters(w/2), radial-90) -- 1 Right of carrier CORRECT! + c[3]=c[2]:Translate( UTILS.NMToMeters(d+w/2), radial) -- 13 "south" @ 1 right + c[4]=c[3]:Translate( UTILS.NMToMeters(y+a/2), radial+90) -- y+x left @ 13 south + c[5]=c[4]:Translate( UTILS.NMToMeters(l), offset) -- 10 NM to back wall (angled) + c[6]=c[5]:Translate( UTILS.NMToMeters(w), offset+90) -- Back wall (angled) + c[7]=c[6]:Translate(-UTILS.NMToMeters(l+a), offset) -- 10+a Back along X & Z + --c[8]=c[7]:Translate( UTILS.NMToMeters(y-a/2), radial-90) -- y-x back along X + c[9]=c[1]:Translate( UTILS.NMToMeters(w/2), radial+90) -- 1 left of carrier CORRECT! + c[8]=c[9]:Translate( UTILS.NMToMeters(d-w/2), radial) -- 1 left and 11 behind of carrier CORRECT! -- Create an array of a square! local p={} @@ -5776,7 +5779,7 @@ function AIRBOSS:_CheckRadioQueue(radioqueue, name) local function _sort(a, b) return (a.Tplay < b.Tplay) or (a.Tplay==b.Tplay and a.prio < b.prio) end - table.sort(radioqueue, _sort) + --table.sort(radioqueue, _sort) local playing=false local next=nil --#AIRBOSS.Radioitem @@ -5861,7 +5864,7 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay) table.insert(self.RQLSO, transmission) - elseif radio:GetAlias()=="AIRBOSS" then + elseif radio:GetAlias()=="MARSHAL" then table.insert(self.RQMarshal, transmission) @@ -5975,18 +5978,31 @@ end -- @param #number duration Display message duration. Default 10 seconds. -- @param #boolean clear If true, clear screen from previous messages. -- @param #number delay Delay in seconds, before the message is displayed. -function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay) +-- @param #boolean soundoff If true, do not play boad number message. +function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay, soundoff) + local playit=true -- In case two have the same flight number. for _,_player in pairs(self.players) do - local player=_player --#AIRBOSS.PlayerData + local playerData=_player --#AIRBOSS.PlayerData -- Message to all players in CCA. -- TODO: could make something to all in pattern or all in marshal queue depending on sender. - if player.unit:IsInZone(self.zoneCCA) then + if playerData.unit:IsInZone(self.zoneCCA) then + + -- Play receiver board number. Best we can do if no voice over for the whole message is there. + if receiver==playerData.onboard and playit and not soundoff then + if sender then + if sender=="LSO" or sender =="AIRBOSS" then + self:_Number2Sound(self.LSOradio, receiver, delay) + elseif sender=="MARSHAL" then + self:_Number2Sound(self.Carrierradio, receiver, delay) + end + end + playit=false -- Play only once, in case two have the same flight number. + end - -- No sound here. - -- TODO: Need to improve? depending on sender. - self:MessageToPlayer(player, message, sender, receiver, duration, clear, delay, true) + -- Message to player. + self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, true) end @@ -6012,14 +6028,18 @@ function AIRBOSS:_Number2Sound(radio, number, delay) return chars end + -- Get radio alias. local alias=radio:GetAlias() + local sender="" if alias=="LSO" then sender="LSOCall" elseif alias=="MARSHAL" then sender="MarshalCall" - elseif alias=="AIRBOSS" then - sender="AirbossCall" + --elseif alias=="AIRBOSS" then + -- sender="AirbossCall" + else + self:E(self.lid.."ERROR: Unknown radio alias!") end -- Split string into characters. @@ -6051,7 +6071,7 @@ function AIRBOSS:_Number2Sound(radio, number, delay) elseif n=="9" then self:RadioTransmission(radio, AIRBOSS[sender].N9, false, delay) else - self:E(self.lid..string.format("ERROR: Unknown number %s", tostring(n))) + self:E(self.lid..string.format("ERROR: Unknown number %s!", tostring(n))) end end @@ -6075,69 +6095,67 @@ function AIRBOSS:_AddF10Commands(_unitName) -- Get group and ID. local group=_unit:GetGroup() - local _gid=group:GetID() + local gid=group:GetID() - if group and _gid then + if group and gid then - if not self.menuadded[_gid] then + if not self.menuadded[gid] then -- Enable switch so we don't do this twice. - self.menuadded[_gid]=true + self.menuadded[gid]=true -- Main F10 menu: F10/Airboss// - if AIRBOSS.MenuF10[_gid]==nil then - AIRBOSS.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "Airboss") + if AIRBOSS.MenuF10[gid]==nil then + AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss") end -- Player Data. local playerData=self.players[playername] - -- F10/Airboss/ - local _rootPath=missionCommands.addSubMenuForGroup(_gid, self.alias, AIRBOSS.MenuF10[_gid]) + -- F10/Airboss/ + local _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) - -- F10/Airboss//Results - local _statsPath=missionCommands.addSubMenuForGroup(_gid, "Results", _rootPath) + -- F10/Airboss//Help + local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) + -- F10/Airboss//Help/Skill Level + local _skillPath=missionCommands.addSubMenuForGroup(gid, "Skill Level", _helpPath) + -- F10/Airboss//Help/Skill Level/ + missionCommands.addCommandForGroup(gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) + missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) + missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) + -- F10/Airboss//Help/Mark Zones + local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) + -- F10/Airboss//Help/Mark Zones/ + missionCommands.addCommandForGroup(gid, "Smoke My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) + missionCommands.addCommandForGroup(gid, "Flare My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) + missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCase23Zones, self, _unitName, false) + missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCase23Zones, self, _unitName, true) + -- F10/Airboss//Help/ + missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) + missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) + missionCommands.addCommandForGroup(gid, "Attitude Monitor ON/OFF", _helpPath, self._AttitudeMonitor, self, playername) + missionCommands.addCommandForGroup(gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) + - -- F10/Airboss//My Settings/Skil Level - local _skillPath=missionCommands.addSubMenuForGroup(_gid, "Skill Level", _rootPath) + -- F10/Airboss//Kneeboard + local _kneeboardPath=missionCommands.addSubMenuForGroup(gid, "Kneeboard", _rootPath) + -- F10/Airboss//Kneeboard/Results + local _resultsPath=missionCommands.addSubMenuForGroup(gid, "Results", _kneeboardPath) + -- F10/Airboss//Kneeboard/Results/ + missionCommands.addCommandForGroup(gid, "Greenie Board", _resultsPath, self._DisplayScoreBoard, self, _unitName) + missionCommands.addCommandForGroup(gid, "My LSO Grades", _resultsPath, self._DisplayPlayerGrades, self, _unitName) + missionCommands.addCommandForGroup(gid, "Last Debrief", _resultsPath, self._DisplayDebriefing, self, _unitName) + -- F10/Airboss//My Settings/Skil Level - local _helpPath=missionCommands.addSubMenuForGroup(_gid, "Help", _rootPath) - -- F10/Airboss//My Settings/Kneeboard - local _kneeboardPath=missionCommands.addSubMenuForGroup(_gid, "Kneeboard", _rootPath) - - -- F10/Airboss//Results/ - missionCommands.addCommandForGroup(_gid, "Greenie Board", _statsPath, self._DisplayScoreBoard, self, _unitName) - missionCommands.addCommandForGroup(_gid, "My LSO Grades", _statsPath, self._DisplayPlayerGrades, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Last Debrief", _statsPath, self._DisplayDebriefing, self, _unitName) - --missionCommands.addCommandForGroup(_gid, "(Clear ALL Results)", _statsPath, self._ResetRangeStats, self, _unitName) - - -- F10/Airboss//Skill Level - missionCommands.addCommandForGroup(_gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) - missionCommands.addCommandForGroup(_gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) - missionCommands.addCommandForGroup(_gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) - - -- F10/Airboss//Help - missionCommands.addCommandForGroup(_gid, "Attitude Monitor ON/OFF", _helpPath, self._AttitudeMonitor, self, playername) - missionCommands.addCommandForGroup(_gid, "Smoke Marshal Zone", _helpPath, self._MarkMarshalZone, self, _unitName, false) - missionCommands.addCommandForGroup(_gid, "Flare Marshal Zone", _helpPath, self._MarkMarshalZone, self, _unitName, true) - missionCommands.addCommandForGroup(_gid, "Smoke Pattern Zones", _helpPath, self._MarkCase23Zones, self, _unitName, false) - missionCommands.addCommandForGroup(_gid, "Flare Pattern Zones", _helpPath, self._MarkCase23Zones, self, _unitName, true) - missionCommands.addCommandForGroup(_gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) - - -- F10/Airboss//Kneeboard - missionCommands.addCommandForGroup(_gid, "Carrier Info", _kneeboardPath, self._DisplayCarrierInfo, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Weather Report", _kneeboardPath, self._DisplayCarrierWeather, self, _unitName) - missionCommands.addCommandForGroup(_gid, "My Status", _kneeboardPath, self._DisplayPlayerStatus, self, _unitName) - - -- F10/Airboss// - missionCommands.addCommandForGroup(_gid, "Request Marshal?", _rootPath, self._RequestMarshal, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Commencing!", _rootPath, self._RequestCommence, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Request Refueling?", _rootPath, self._RequestRefueling, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Set Section!", _rootPath, self._SetSection, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Radio Check LSO", _rootPath, self._LSORadioCheck, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Radio Check Marshal", _rootPath, self._MarshalRadioCheck,self, _unitName) + -- F10/Airboss// + missionCommands.addCommandForGroup(gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) + missionCommands.addCommandForGroup(gid, "Request Commencing", _rootPath, self._RequestCommence, self, _unitName) + missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) + missionCommands.addCommandForGroup(gid, "Set Section", _rootPath, self._SetSection, self, _unitName) end else self:T(self.lid.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) @@ -7007,23 +7025,23 @@ function AIRBOSS:_MarkCase23Zones(_unitName, flare) -- Case II/III: arc in/out if offset>0. if case==2 or case==3 then if math.abs(self.holdingoffset)>0 then - self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Red, 45) - text=text.."* arc turn in with YELLOW flares\n" - self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Orange, 45) - text=text.."* arc trun out with WHITE flares\n" + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) + text=text.."* arc turn in with BLUE smoke\n" + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) + text=text.."* arc trun out with BLUE smoke\n" end end -- Case III: dirty up if case==3 then - text=text.."* dirty up with ORANGE flares\n" + text=text.."* dirty up with ORANGE smoke\n" self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) end - -- Case III: dirty up + -- Case III: bullseye if case==3 then - text=text.."* bullseye with BLUE smoke\n" - self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Blue, 45) + text=text.."* bullseye with WHITE smoke\n" + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) end end diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 44b615e3d..e6babf046 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -179,7 +179,7 @@ -- @field #RECOVERYTANKER RECOVERYTANKER = { ClassName = "RECOVERYTANKER", - Debug = false, + Debug = true, carrier = nil, carriertype = nil, tankergroupname = nil, @@ -210,7 +210,7 @@ RECOVERYTANKER = { --- Class version. -- @field #string version -RECOVERYTANKER.version="0.9.5w" +RECOVERYTANKER.version="0.9.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -271,8 +271,10 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetPatternUpdateHeading() self:SetPatternUpdateInterval() - -- Moving zone: Zone 1 NM astern the carrier with radius of 1.0 km. - self.zoneUpdate=ZONE_UNIT:New("Pattern Update Zone", self.carrier, 1*1000, {dx=-UTILS.NMToMeters(1), dy=0, relative_to_unit=true}) + -- Moving zone: Zone 1 NM astern the carrier with radius of 1 NM. + self.zoneUpdate=ZONE_UNIT:New("Pattern Update Zone", self.carrier, UTILS.NMToMeters(1), {dx=-UTILS.NMToMeters(1), dy=0, relative_to_unit=true}) + + self.zoneUpdate:SmokeZone(SMOKECOLOR.White, 45) ----------------------- --- FSM Transitions --- @@ -629,7 +631,6 @@ function RECOVERYTANKER:onafterStart(From, Event, To) self:HandleEvent(EVENTS.EngineShutdown) self:HandleEvent(EVENTS.Refueling, self._RefuelingStart) --Need explcit functions sice OnEventRefueling and OnEventRefuelingStop did not hook. self:HandleEvent(EVENTS.RefuelingStop, self._RefuelingStop) - --self:HandleEvent(EVENTS.Crash) -- Spawn tanker. local Spawn=SPAWN:New(self.tankergroupname):InitUnControlled(false) @@ -710,7 +711,7 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) -- Get fuel of tanker. local fuel=self.tanker:GetFuel()*100 local text=string.format("Recovery tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) - self:T(text) + self:I(text) -- Check if tanker flies through pattern update zone. -- TODO: Check if this can be used to update the pattern without too much disruption. @@ -800,7 +801,7 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) local Carrier=self.carrier:GetCoordinate() -- Define race-track pattern. - local p0=self.tanker:GetCoordinate():Translate(2000, self.tanker:GetHeading()) + local p0=self.tanker:GetCoordinate():Translate(3000, self.tanker:GetHeading()) local p1=Carrier:SetAltitude(self.altitude):Translate(self.distStern, hdg) local p2=Carrier:SetAltitude(self.altitude):Translate(self.distBow, hdg) @@ -821,6 +822,8 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil , self.speed, {}, "Current Position") wp[2]=p0:WaypointAirTurningPoint(nil, self.speed, {taskorbit}, "Tanker Orbit") + --local wp=self:_Pattern() + -- Initialize WP and route tanker. self.tanker:WayPointInitialize(wp) @@ -837,6 +840,48 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) self.Tupdate=timer.getTime() end + +--- Self made race track pattern. +-- @param #RECOVERYTANKER self +-- @return #table Table of pattern waypoints. +function RECOVERYTANKER:_Pattern() + + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- Pattern altitude + local alt=self.altitude + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + local width=UTILS.NMToMeters(8) + + -- Not working as desired, since tanker changes course too rapidly after each waypoint. + + -- Define race-track pattern. + local p={} + p[1]=self.tanker:GetCoordinate() -- Tanker position + p[2]=Carrier:SetAltitude(alt) -- Carrier position + p[3]=p[2]:Translate(self.distBow, hdg) -- In front of carrier + p[4]=p[3]:Translate(width/math.sqrt(2), hdg-45) -- Middle front for smoother curve + -- Probably need one more to make it go -hdg at the waypoint. + p[5]=p[3]:Translate(width, hdg-90) -- In front on port + p[6]=p[5]:Translate(self.distStern-self.distBow, hdg) -- Behind on port (sterndist<0!) + p[7]=p[2]:Translate(self.distStern, hdg) -- Behind carrier + + local wp={} + for i=1,#p do + local coord=p[i] --Core.Point#COORDINATE + coord:MarkToAll(string.format("Waypoint %d", i)) + --table.insert(wp, coord:WaypointAirFlyOverPoint(nil , self.speed)) + table.insert(wp, coord:WaypointAirTurningPoint(nil , self.speed)) + end + + return wp +end + --- On after "RTB" event. Send tanker back to carrier. -- @param #RECOVERYTANKER self -- @param #string From From state. @@ -1029,7 +1074,7 @@ function RECOVERYTANKER:_InitRoute(dist, delay) -- Waypoints. local wp={} if self.takeoff==SPAWN.Takeoff.Air then - wp[#wp+1]=self.tanker:GetCoordinate():SetAltitude(self.altitude):WaypointAirTurningPoint(nil, self.speed, {}, "Spawn Position") + wp[#wp+1]=self.tanker:GetCoordinate():SetAltitude(self.altitude):WaypointAirTurningPoint(nil, self.speed, {}, "Spawn Position") else wp[#wp+1]=Carrier:WaypointAirTakeOffParking() end @@ -1131,7 +1176,7 @@ function RECOVERYTANKER:_ActivateTACAN(delay) end --- Calculate distances between carrier and tanker. --- @param #AIRBOSS self +-- @param #RECOVERYTANKER self -- @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. From 302d9dfad4094bdcc9c28ef07fbbbcc700d7b05f Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 5 Dec 2018 15:57:26 +0100 Subject: [PATCH 077/147] AIRBOSS v0.4.5w --- Moose Development/Moose/Ops/Airboss.lua | 193 ++++++++++++------------ 1 file changed, 99 insertions(+), 94 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 91d45c96d..09cf8fb51 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -15,7 +15,7 @@ -- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, help function (player aircraft attitude, marking of pattern zones etc). -- * Recovery tanker and refueling option via integration of @{#Ops.RecoveryTanker} class. -- * Rescue helo option via @{#Ops.RescueHelo} class. --- * Multiple carriers supported (due to object oriented approach). +-- * Multiple carrier support (due to object oriented approach). -- -- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much work in progress. -- @@ -67,9 +67,9 @@ -- @field #AIRBOSS.Checkpoint BreakLate Late brak checkpoint. -- @field #AIRBOSS.Checkpoint Abeam Abeam checkpoint. -- @field #AIRBOSS.Checkpoint Ninety At the ninety checkpoint. --- @field #AIRBOSS.Checkpoint Wake Right behind the carrier. +-- @field #AIRBOSS.Checkpoint Wake Checkpoint right behind the carrier. +-- @field #AIRBOSS.Checkpoint Final Checkpoint when turning to final. -- @field #AIRBOSS.Checkpoint Groove In the groove checkpoint. --- @field #AIRBOSS.Checkpoint Trap Landing checkpoint. -- @field #AIRBOSS.Checkpoint Platform Case II/III descent at 2000 ft/min at 5000 ft platform. -- @field #AIRBOSS.Checkpoint DirtyUp Case II/III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. -- @field #AIRBOSS.Checkpoint Bullseye Case III intercept glideslope and follow ICLS aka "bullseye". @@ -158,8 +158,8 @@ AIRBOSS = { BreakLate = {}, Ninety = {}, Wake = {}, + Final = {}, Groove = {}, - Trap = {}, Platform = {}, DirtyUp = {}, Bullseye = {}, @@ -380,7 +380,7 @@ AIRBOSS.LSOCall={ duration=1.0, }, LONGINGROOVE={ - file="LSO-LonInTheGroove", + file="LSO-LongInTheGroove", suffix="ogg", louder=false, subtitle="You're long in the groove", @@ -684,7 +684,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.5" +AIRBOSS.version="0.4.5w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1233,7 +1233,6 @@ function AIRBOSS:onafterStatus(From, Event, To) self:_CheckPlayerStatus() -- Call status every 0.5 seconds. - -- TODO: make dt user input. self:__Status(-0.5) end @@ -1402,9 +1401,8 @@ function AIRBOSS:_InitStennis() self.Platform.Xmax =nil self.Platform.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. self.Platform.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. - self.Platform.LimitXmin=nil - --TODO: better rho dist! now switch to dirty up level flight 12 NM. - self.Platform.LimitXmax=-UTILS.NMToMeters(20) -- Check and next step when 20 NM behind the boat. + self.Platform.LimitXmin=nil -- Limits via zone + self.Platform.LimitXmax=nil self.Platform.LimitZmin=nil self.Platform.LimitZmax=nil @@ -1414,9 +1412,8 @@ function AIRBOSS:_InitStennis() self.DirtyUp.Xmax= nil self.DirtyUp.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. self.DirtyUp.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. - self.DirtyUp.LimitXmin=nil - --TODO: better rho dist! Intercept glideslope and follow bullseye. - self.DirtyUp.LimitXmax=-UTILS.NMToMeters(10) -- Check and next step at 10 NM behind the boat. + self.DirtyUp.LimitXmin=nil -- Limits via zone + self.DirtyUp.LimitXmax=nil self.DirtyUp.LimitZmin=nil self.DirtyUp.LimitZmax=nil @@ -1426,9 +1423,8 @@ function AIRBOSS:_InitStennis() self.Bullseye.Xmax= nil self.Bullseye.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port. self.Bullseye.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard. - self.Bullseye.LimitXmin=nil - --TODO: better rho dist! Call the ball. - self.Bullseye.LimitXmax=-UTILS.NMToMeters(3) -- Check and next step 3 NM behind the boat. + self.Bullseye.LimitXmin=nil -- Limits via zone. + self.Bullseye.LimitXmax=nil self.Bullseye.LimitZmin=nil self.Bullseye.LimitZmax=nil @@ -1498,29 +1494,27 @@ function AIRBOSS:_InitStennis() self.Wake.LimitZmin=0 -- Check and next step when directly behind the boat. self.Wake.LimitZmax=nil - -- TODO: rename to final -- Turn to final. + self.Final.name="Final" + self.Final.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Final.Xmax= 0 -- Must be behind the boat. + self.Final.Zmin=-1000 -- Not more than 1 km port. + self.Final.Zmax= nil + self.Final.LimitXmin=nil -- No limits. Check is carried out differently. + self.Final.LimitXmax=nil + self.Final.LimitZmin=nil + self.Final.LimitZmax=nil + + -- In the Groove. self.Groove.name="Groove" self.Groove.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. - self.Groove.Xmax= 0 -- Must be behind the boat. - self.Groove.Zmin=-1000 -- Not more than 1 km port. - self.Groove.Zmax= nil + self.Groove.Xmax= nil + self.Groove.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port + self.Groove.Zmax= UTILS.NMToMeters(2) -- Not more than 2 NM starboard. self.Groove.LimitXmin=nil -- No limits. Check is carried out differently. self.Groove.LimitXmax=nil self.Groove.LimitZmin=nil self.Groove.LimitZmax=nil - - -- TODO rename to groove - -- In the Groove. - self.Trap.name="Trap" - self.Trap.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. - self.Trap.Xmax= nil - self.Trap.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port - self.Trap.Zmax= UTILS.NMToMeters(2) -- Not more than 2 NM starboard. - self.Trap.LimitXmin=nil -- No limits. Check is carried out differently. - self.Trap.LimitXmax=nil - self.Trap.LimitZmin=nil - self.Trap.LimitZmax=nil end @@ -3328,7 +3322,6 @@ function AIRBOSS:_DirtyUp(playerData) -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) - --if self:_CheckLimits(X, Z, self.DirtyUp) then if inzone then -- Debug message. @@ -3689,8 +3682,8 @@ function AIRBOSS:_Final(playerData) local X, Z, rho, phi = self:_GetDistances(playerData.unit) -- In front of carrier or more than 4 km behind carrier. - if self:_CheckAbort(X, Z, self.Groove) then - self:_AbortPattern(playerData, X, Z, self.Groove, true) + if self:_CheckAbort(X, Z, self.Final) then + self:_AbortPattern(playerData, X, Z, self.Final, true) return end @@ -3763,8 +3756,8 @@ function AIRBOSS:_Groove(playerData) local player=playerData.unit:GetGroup() -- Check abort conditions. - if self:_CheckAbort(X, Z, self.Trap) then - self:_AbortPattern(playerData, X, Z, self.Trap, true) + if self:_CheckAbort(X, Z, self.Groove) then + self:_AbortPattern(playerData, X, Z, self.Groove, true) return end @@ -3802,7 +3795,7 @@ function AIRBOSS:_Groove(playerData) -- Pilot "405, Hornet Ball, 3.2" -- TODO: Pilot output should come from pilot in MP. - local text=string.format("Hornet Ball, %.1f", self:_GetFuelState(playerData.unit)) + local text=string.format("Hornet Ball, %.1f", self:_GetFuelState(playerData.unit)/1000) self:MessageToPlayer(playerData, text, playerData.onboard, "", 3, false, 3) -- Store data. @@ -3932,29 +3925,35 @@ end -- @param #number glideslopeError Glide slope error in degrees. -- @param #number lineupError Line up error in degrees. -- @param #number AoA Angle of attack of player aircraft. --- @param #string diffifulty Difficulty setting of player. +-- @param #AIRBOSS.PlayerData playerData Player data. -- @return #boolean If true, player should wave off! -function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, difficulty) +function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) + -- Assume we're all good. local waveoff=false -- Too high or too low? if math.abs(glideslopeError)>1 then - self:I(self.lid..string.format("Wave off due to glide slope error %.1f > 1 degree!", glideslopeError)) + self:I(self.lid..string.format("%s: Wave off due to glide slope error %.1f > 1 degree!", playerData.name, glideslopeError)) waveoff=true end -- Too far from centerline? if math.abs(lineupError)>3 then - self:I(self.lid..string.format("Wave off due to line up error %.1f > 3 degrees!", lineupError)) + self:I(self.lid..string.format("%s: Wave off due to line up error %.1f > 3 degrees!", playerData.name, lineupError)) waveoff=true end - -- Too slow or too fast? - --TODO: Get aircraft dependent values. Needs playerData! - if AoA<6.9 or AoA>9.3 then - if difficulty==AIRBOSS.Difficulty.HARD then - self:I(self.lid.."Wave off due to AoA<6.9 or AoA>9.3!") + -- Too slow or too fast? Only for pros. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- Get aircraft specific AoA values + local aoaac=self:_GetAircraftAoA(playerData) + -- Check too slow or too fast. + if AoAaoaac.Slow then + self:I(self.lid..string.format("%s: Wave off due to AoA %.1f > %.1f!", playerData.name, AoA, aoaac.Slow)) waveoff=true end end @@ -5937,6 +5936,7 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration -- Format message. local text if receiver and receiver=="" then + -- No (blank) receiver. text=string.format("%s", message) else -- Default "receiver" is onboard number of player. @@ -5945,7 +5945,7 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration end self:I(self.lid..text) - -- TODO: Test! Need to make this better!. + -- Send onboard number so that player is alerted about the text message. -- DONE: This will fail with message to all since for each player the message will be played! if receiver==playerData.onboard and not soundoff then if sender then @@ -5990,13 +5990,14 @@ function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay, if playerData.unit:IsInZone(self.zoneCCA) then -- Play receiver board number. Best we can do if no voice over for the whole message is there. - if receiver==playerData.onboard and playit and not soundoff then - if sender then - if sender=="LSO" or sender =="AIRBOSS" then - self:_Number2Sound(self.LSOradio, receiver, delay) - elseif sender=="MARSHAL" then - self:_Number2Sound(self.Carrierradio, receiver, delay) - end + if receiver==playerData.onboard and sender and playit and not soundoff then + -- Check who is the sender. + if sender=="LSO" or sender =="AIRBOSS" then + -- Sender is LSO or AIRBOSS ==> Broadcast on LSO radio. + self:_Number2Sound(self.LSOradio, receiver, delay) + elseif sender=="MARSHAL" then + -- Sender is MARSHAL ==> Broadcast on MARSHAL radio. + self:_Number2Sound(self.Carrierradio, receiver, delay) end playit=false -- Play only once, in case two have the same flight number. end @@ -6236,9 +6237,8 @@ function AIRBOSS:_MarshalRadioCheck(_unitName) if _unit and _playername then local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then - -- Broadcase LSO radio check message on LSO radio. - -- TODO: Replace LSO message by marshal message. - self:RadioTransmission(self.Carrierradio, AIRBOSS.LSOCall.RADIOCHECK) + -- Broadcase Marshal radio check message on Marshal radio. + self:RadioTransmission(self.Carrierradio, AIRBOSS.MarshalCall.RADIOCHECK) end end end @@ -6373,7 +6373,6 @@ function AIRBOSS:_RequestCommence(_unitName) else -- This flight is not yet registered! text="Negative ghostrider, you are not inside the CCA yet!" - -- TODO: fly 10 km towards the carrier advice for skill "Flight Student" end -- Debug @@ -6465,42 +6464,48 @@ function AIRBOSS:_SetSection(_unitName) -- Coordinate of flight lead. local mycoord=_unit:GetCoordinate() - -- TODO: Only allow set section, if player is not in marshal stack yet. - - -- Loop over all registered flights. - for _,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.Flightitem - - -- Only human flight groups excluding myself. - if flight.ai==false and flight.groupname~=playerData.groupname then - - -- Distance to other group. - local distance=flight.group:GetCoordinate():Get2DDistance(mycoord) - - if distance<200 then - table.insert(playerData.section, flight) - end - - end - end - - -- Info on section members. + -- Check if player is in Marshal or pattern queue already. local text - if #playerData.section>0 then - text=string.format("Registered flight section:") - text=text..string.format("- %s (lead)", playerData.name) - for _,_flight in paris(playerData.section) do - local flight=_flight --#AIRBOSS.PlayerData - text=text..string.format("- %s", flight.name) - flight.seclead=playerData.name - - -- Inform player that he is now part of a section. - self:MessageToPlayer(flight, string.format("Your section lead is now %s.", playerData.name), "MARSHAL") - end + if self:_InQueue(self.Qmarshal,playerData.group) then + text=string.format("You are already in the Marshal queue. Setting section no possible any more!") + elseif self:_InQueue(self.Qpattern, playerData.group) then + text=string.format("You are already in the Pattern queue. Setting section no possible any more!") else - text="No other human flights found within radius of 200 meters!" + + -- Loop over all registered flights. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.Flightitem + + -- Only human flight groups excluding myself. + if flight.ai==false and flight.groupname~=playerData.groupname then + + -- Distance to other group. + local distance=flight.group:GetCoordinate():Get2DDistance(mycoord) + + if distance<200 then + table.insert(playerData.section, flight) + end + + end + end + + -- Info on section members. + if #playerData.section>0 then + text=string.format("Registered flight section:") + text=text..string.format("- %s (lead)", playerData.name) + for _,_flight in paris(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + text=text..string.format("- %s", flight.name) + flight.seclead=playerData.name + + -- Inform player that he is now part of a section. + self:MessageToPlayer(flight, string.format("Your section lead is now %s.", playerData.name), "MARSHAL") + end + else + text="No other human flights found within radius of 200 meters!" + end end - + -- Message to section lead. self:MessageToPlayer(playerData, text, "MARSHAL") end @@ -6757,7 +6762,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) text=text..string.format("BRC %03d°\n", self:GetBRC()) text=text..string.format("FB %03d°\n", self:GetFinalBearing(true)) text=text..string.format("Speed %d kts\n", carrierspeed) - text=text..string.format("Airboss radio %.3f MHz\n", self.Carrierfreq) --TODO: add modulation + text=text..string.format("Marshal radio %.3f MHz\n", self.Carrierfreq) --TODO: add modulation text=text..string.format("LSO radio %.3f MHz\n", self.LSOfreq) text=text..string.format("TACAN Channel %s\n", tacan) text=text..string.format("ICLS Channel %s\n", icls) @@ -6942,7 +6947,7 @@ function AIRBOSS:_MarkMarshalZone(_unitName, flare) end -- Send message to player. - self:MessageToPlayer(playerData, text, "AIRBOSS", "", 10) + self:MessageToPlayer(playerData, text, "MARSHAL", "", 10) end end From c2ddf17aa2dc78cd1a60465f78bb3eed5d107a30 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 5 Dec 2018 23:35:03 +0100 Subject: [PATCH 078/147] AIRBOS v0.4.6 --- Moose Development/Moose/Ops/Airboss.lua | 277 +++++++++++++----------- 1 file changed, 151 insertions(+), 126 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 09cf8fb51..7670d53ef 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -125,7 +125,7 @@ -- @field #AIRBOSS AIRBOSS = { ClassName = "AIRBOSS", - Debug = false, + Debug = true, lid = nil, carrier = nil, carriertype = nil, @@ -309,54 +309,54 @@ AIRBOSS.LSOCall={ suffix="ogg", loud=true, subtitle="Right for line up", - duration=1.0, + duration=0.80, }, COMELEFT={ file="LSO-ComeLeft", suffix="ogg", loud=true, subtitle="Come left", - duration=0.8, + duration=0.60, }, HIGH={ file="LSO-High", loud=true, subtitle="You're high", - duration=0.9, + duration=0.65, }, LOW={ file="LSO-Low", loud=true, subtitle="You're low", - duration=0.6, + duration=0.50, }, POWER={ file="LSO-Power", suffix="ogg", loud=true, subtitle="Power", - duration=0.6, + duration=0.45, }, SLOW={ file="LSO-Slow", suffix="ogg", loud=true, subtitle="You're slow", - duration=0.9, + duration=0.65, }, FAST={ file="LSO-Fast", suffix="ogg", loud=true, subtitle="You're fast", - duration=0.9, + duration=0.7, }, CALLTHEBALL={ file="LSO-CallTheBall", suffix="ogg", louder=false, subtitle="Call the ball", - duration=0.7, + duration=0.6, }, ROGERBALL={ file="LSO-RogerBall", @@ -370,28 +370,28 @@ AIRBOSS.LSOCall={ suffix="ogg", louder=false, subtitle="Wave off", - duration=0.7, + duration=0.6, }, BOLTER={ file="LSO-BolterBolter", suffix="ogg", louder=false, subtitle="Bolter, Bolter!", - duration=1.0, + duration=0.75, }, LONGINGROOVE={ file="LSO-LongInTheGroove", suffix="ogg", louder=false, subtitle="You're long in the groove", - duration=1.3, + duration=1.2, }, DEPARTANDREENTER={ file="LSO-DepartAndReenter", suffix="ogg", louder=false, subtitle="Depart and re-enter", - duration=1.3, + duration=1.1, }, PADDLESCONTACT={ file="LSO-PaddlesContact", @@ -405,77 +405,77 @@ AIRBOSS.LSOCall={ suffix="ogg", louder=false, subtitle="Paddles, radio check", - duration=1.0, + duration=1.1, }, N0={ file="LSO-N0", suffix="ogg", louder=false, subtitle="0", - duration=0.5, + duration=0.40, }, N1={ file="LSO-N1", suffix="ogg", louder=false, subtitle="1", - duration=0.3, + duration=0.25, }, N2={ file="LSO-N2", suffix="ogg", louder=false, subtitle="2", - duration=0.3, + duration=0.35, }, N3={ file="LSO-N3", suffix="ogg", louder=false, subtitle="3", - duration=0.4, + duration=0.37, }, N4={ file="LSO-N4", suffix="ogg", louder=false, subtitle="4", - duration=0.4, + duration=0.39, }, N5={ file="LSO-N5", suffix="ogg", louder=false, subtitle="5", - duration=0.4, + duration=0.38, }, N6={ file="LSO-N6", suffix="ogg", louder=false, subtitle="6", - duration=0.6, + duration=0.40, }, N7={ file="LSO-N7", suffix="ogg", louder=false, subtitle="7", - duration=0.6, + duration=0.40, }, N8={ file="LSO-N8", suffix="ogg", louder=false, subtitle="8", - duration=0.4, + duration=0.37, }, N9={ file="LSO-N9", suffix="ogg", louder=false, subtitle="9", - duration=0.5, + duration=0.38, }, } @@ -492,75 +492,83 @@ AIRBOSS.LSOCall={ -- @field #AIRBOSS.RadioSound N9 "Nine" call. -- @field #AIRBOSS.RadioSound N0 "Zero" call. AIRBOSS.MarshalCall={ + RADIOCHECK={ + file="MARSHAL-RadioCheck", + suffix="ogg", + louder=false, + subtitle="Marshal, radio check", + duration=1.0, + }, +-- TODO: Other voice overs for marshal. N0={ file="LSO-N0", suffix="ogg", louder=false, subtitle="0", - duration=0.5, + duration=0.40, }, N1={ file="LSO-N1", suffix="ogg", louder=false, subtitle="1", - duration=0.3, + duration=0.25, }, N2={ file="LSO-N2", suffix="ogg", louder=false, subtitle="2", - duration=0.3, + duration=0.35, }, N3={ file="LSO-N3", suffix="ogg", louder=false, subtitle="3", - duration=0.4, + duration=0.37, }, N4={ file="LSO-N4", suffix="ogg", louder=false, subtitle="4", - duration=0.4, + duration=0.39, }, N5={ file="LSO-N5", suffix="ogg", louder=false, subtitle="5", - duration=0.4, + duration=0.38, }, N6={ file="LSO-N6", suffix="ogg", louder=false, subtitle="6", - duration=0.6, + duration=0.40, }, N7={ file="LSO-N7", suffix="ogg", louder=false, subtitle="7", - duration=0.6, + duration=0.40, }, N8={ file="LSO-N8", suffix="ogg", louder=false, subtitle="8", - duration=0.4, + duration=0.37, }, N9={ file="LSO-N9", suffix="ogg", louder=false, subtitle="9", - duration=0.5, + duration=0.38, }, } @@ -684,7 +692,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.5w" +AIRBOSS.version="0.4.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -790,10 +798,10 @@ function AIRBOSS:New(carriername, alias) self:SetMaxLandingPattern(2) -- Set holding offset to 0 degrees. - self:SetHoldingOffsetAngle(45) + self:SetHoldingOffsetAngle(15) -- Default recovery case. - self:SetRecoveryCase(1) + self:SetRecoveryCase(3) -- CCA 50 NM radius zone around the carrier. self:SetCarrierControlledArea() @@ -1192,8 +1200,8 @@ function AIRBOSS:onafterStart(From, Event, To) -- Schedule radio queue checks. -- TODO: id's to self to be able to stop the scheduler. - local RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQLSO, "LSO"}, 1, 0.1) - local RQMid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQMarshal, "MARSHAL"}, 1, 0.1) + local RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQLSO, "LSO"}, 1, 0.01) + local RQMid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQMarshal, "MARSHAL"}, 1, 0.01) -- Start status check in 1 second. self:__Status(1) @@ -2807,9 +2815,8 @@ function AIRBOSS:OnEventBirth(EventData) -- Debug. if self.Debug then --- self:_Number2Sound(self.LSOradio, "0123456789", 10) - self:_Number2Sound(self.Carrierradio, "0123456789", 10) - --self:_MarkCase23Zones(_unit:GetName()) + self:_Number2Sound(self.LSOradio, "0123456789", 10) + self:_Number2Sound(self.Carrierradio, "0123456789", 30) end end @@ -3135,13 +3142,13 @@ function AIRBOSS:_Platform(playerData) -- Issue warning. if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "MARSHAL") playerData.warning=true end -- Back in zone. if not invalid and playerData.warning then - self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "AIRBOSS") + self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARSHAL") playerData.warning=false end @@ -3200,13 +3207,13 @@ function AIRBOSS:_ArcInTurn(playerData) -- Issue warning. if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "MARSHAL") playerData.warning=true end -- Back in zone. if not invalid and playerData.warning then - self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "AIRBOSS") + self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARSHAL") playerData.warning=false end @@ -3249,13 +3256,13 @@ function AIRBOSS:_ArcOutTurn(playerData) -- Issue warning. if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "MARHAL") playerData.warning=true end -- Back in zone. if not invalid and playerData.warning then - self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "AIRBOSS") + self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARSHAL") playerData.warning=false end @@ -3308,13 +3315,13 @@ function AIRBOSS:_DirtyUp(playerData) -- Issue warning. if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "MARSHAL") playerData.warning=true end -- Back in zone. if not invalid and playerData.warning then - self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "AIRBOSS") + self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARSHAL") playerData.warning=false end @@ -3362,13 +3369,13 @@ function AIRBOSS:_Bullseye(playerData) -- Issue warning. if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "AIRBOSS") + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "MARSHAL") playerData.warning=true end -- Back in zone. if not invalid and playerData.warning then - self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "AIRBOSS") + self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARHAL") playerData.warning=false end @@ -3823,7 +3830,7 @@ function AIRBOSS:_Groove(playerData) -- Debug. local text=string.format("Groove IM=%d m", rho) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:I(self.lid..string.format("FF IM=%d", rho)) + self:I(self.lid..text) -- Store data. playerData.groove.IM=groovedata @@ -4078,7 +4085,7 @@ function AIRBOSS:_GetZoneDirtyUp(case) -- Radius = 1 NM. local radius=UTILS.NMToMeters(1) - -- Distance = 19 NM + -- Distance = 9 NM local distance=UTILS.NMToMeters(9) -- Zone depends on Case recovery. @@ -4173,8 +4180,10 @@ function AIRBOSS:_GetZoneArcIn(case) end + -- Angle between FB/BRC and holding zone. local alpha=math.rad(self.holdingoffset) + -- 12+x NM from carrier local x=12/math.cos(alpha) -- Distance = 14 NM @@ -4198,10 +4207,7 @@ function AIRBOSS:_GetZonePlatform(case) -- Radius = 1 NM. local radius=UTILS.NMToMeters(1) - - -- Distance = 19 NM - local distance=UTILS.NMToMeters(19) - + -- Zone depends on Case recovery. local radial if case==2 then @@ -4218,6 +4224,12 @@ function AIRBOSS:_GetZonePlatform(case) return nil end + + -- Angle between FB/BRC and holding zone. + local alpha=math.rad(self.holdingoffset) + + -- Distance = 19 NM + local distance=UTILS.NMToMeters(19)/math.cos(alpha) -- Get coordinate and vec2. local coord=self:GetCoordinate():Translate(distance, radial) @@ -4251,58 +4263,66 @@ function AIRBOSS:_GetZoneCorridor(case) radial=self:GetRadialCase3(false, false) offset=self:GetRadialCase3(false, true) end - - self:I(string.format("FF case %d radial = %d", case, radial)) - self:I(string.format("FF case %d offset = %d", case, offset)) - - -- Width of the box in NM. - local w=2 - - -- Length of the box in NM. - local l=10 - + -- Angle between radial and offset in rad. local alpha=math.rad(self.holdingoffset) + + -- Width of the box in NM. + local w=2 + local w2=w/2 - -- Distance from carrier to arc zone. + -- Length of the box in NM. + local l=10/math.cos(alpha) + + -- Distance from carrier to arc out zone. local d=12 - -- Distance from ArcIn to ArcOut zone - local y=d*math.tan(alpha) + -- Some math... + local y1=d-w2 + local x1=y1*math.tan(alpha) + local y2=d+w2 + local x2=y2*math.tan(alpha) + local b=w2*(1/math.cos(alpha)-1) - -- Little extra bit along X. - local x=w/2*math.tan(alpha) + -- This is what we need. + local P=x1+b + local Q=x2-b - -- Get the extra bit we need to go back from the end to the arc turn in. - local C=w/math.cos(alpha) - local b=w*math.tan(alpha) - local a=C-b + -- Debug output. + self:I(string.format("FF case %d radial = %d", case, radial)) + self:I(string.format("FF case %d offset = %d", case, offset)) + self:I(string.format("FF w = %.1f NM", w)) + self:I(string.format("FF l = %.1f NM", l)) + self:I(string.format("FF d = %.1f NM", d)) + self:I(string.format("FF y1 = %.1f NM", y1)) + self:I(string.format("FF x1 = %.1f NM", x1)) + self:I(string.format("FF y2 = %.1f NM", y2)) + self:I(string.format("FF x2 = %.1f NM", x2)) + self:I(string.format("FF b = %.1f NM", b)) + self:I(string.format("FF P = %.1f NM", P)) + self:I(string.format("FF Q = %.1f NM", Q)) - local k=w/2/math.cos(alpha) - - self:I(string.format("FF w = %.1f NM", w)) - self:I(string.format("FF l = %.1f NM", l)) - self:I(string.format("FF d = %.1f NM", d)) - self:I(string.format("FF y = %.1f NM", y)) - self:I(string.format("FF C = %.1f NM", C)) - self:I(string.format("FF b = %.1f NM", b)) - self:I(string.format("FF a = %.1f NM", a)) - self:I(string.format("FF x = %.1f NM", x)) - self:I(string.format("FF k = %.1f NM", k)) - - -- TODO: Still not right! - -- TODO: Does this still work with alpha=0?e local c={} - c[1]=self:GetCoordinate() -- Carrier coordinate - c[2]=c[1]:Translate( UTILS.NMToMeters(w/2), radial-90) -- 1 Right of carrier CORRECT! - c[3]=c[2]:Translate( UTILS.NMToMeters(d+w/2), radial) -- 13 "south" @ 1 right - c[4]=c[3]:Translate( UTILS.NMToMeters(y+a/2), radial+90) -- y+x left @ 13 south - c[5]=c[4]:Translate( UTILS.NMToMeters(l), offset) -- 10 NM to back wall (angled) - c[6]=c[5]:Translate( UTILS.NMToMeters(w), offset+90) -- Back wall (angled) - c[7]=c[6]:Translate(-UTILS.NMToMeters(l+a), offset) -- 10+a Back along X & Z - --c[8]=c[7]:Translate( UTILS.NMToMeters(y-a/2), radial-90) -- y-x back along X - c[9]=c[1]:Translate( UTILS.NMToMeters(w/2), radial+90) -- 1 left of carrier CORRECT! - c[8]=c[9]:Translate( UTILS.NMToMeters(d-w/2), radial) -- 1 left and 11 behind of carrier CORRECT! + c[1]=self:GetCoordinate() --Carrier coordinate + + if math.abs(self.holdingoffset)>1 then + -- Complicated case with an angle. + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier CORRECT! + c[3]=c[2]:Translate( UTILS.NMToMeters(d+w2), radial) -- 13 "south" @ 1 right + c[4]=c[3]:Translate( UTILS.NMToMeters(Q), radial+90) -- + c[5]=c[4]:Translate( UTILS.NMToMeters(l), offset) + c[6]=c[5]:Translate( UTILS.NMToMeters(w), offset+90) -- Back wall (angled) + c[9]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) -- 1 left of carrier CORRECT! + c[8]=c[9]:Translate( UTILS.NMToMeters(d-w2), radial) -- 1 left and 11 behind of carrier CORRECT! + c[7]=c[8]:Translate( UTILS.NMToMeters(P), radial+90) + else + -- Easy case of a long box. + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) + c[3]=c[2]:Translate( UTILS.NMToMeters(d+w2+l), radial) + c[4]=c[3]:Translate( UTILS.NMToMeters(w), radial+90) + c[5]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) + end + -- Create an array of a square! local p={} @@ -4342,8 +4362,6 @@ function AIRBOSS:_GetZoneHolding(case, stack) -- CASE I -- Zone 2.5 NM port of carrier with a radius of 3 NM (holding pattern should be < 5 NM). - --zoneHolding=ZONE_UNIT:New("CASE I Holding Zone", self.carrier, UTILS.NMToMeters(3), {dx=0, dy=-UTILS.NMToMeters(2.5), relative_to_unit=true}) - local R=UTILS.MetersToNM(2.5) local coord=self:GetCoordinate():Translate(R, 270) @@ -5139,7 +5157,7 @@ function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) end -- Message to player. - self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 20) + self:MessageToPlayer(playerData, text, "LSO", nil, 20) end @@ -5464,7 +5482,7 @@ function AIRBOSS:_Debrief(playerData) self:_RemoveUnitFromFlight(playerData.unit) -- Message to player. - self:MessageToPlayer(playerData, string.format("Welcome aboard, %s!", playerData.name), "AIRBOSS", "", 10) + self:MessageToPlayer(playerData, string.format("Welcome aboard, %s!", playerData.name), "LSO", "", 10) else @@ -5949,7 +5967,7 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration -- DONE: This will fail with message to all since for each player the message will be played! if receiver==playerData.onboard and not soundoff then if sender then - if sender=="LSO" or sender =="AIRBOSS" then + if sender=="LSO" then self:_Number2Sound(self.LSOradio, receiver, delay) elseif sender=="MARSHAL" then self:_Number2Sound(self.Carrierradio, receiver, delay) @@ -5992,7 +6010,7 @@ function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay, -- Play receiver board number. Best we can do if no voice over for the whole message is there. if receiver==playerData.onboard and sender and playit and not soundoff then -- Check who is the sender. - if sender=="LSO" or sender =="AIRBOSS" then + if sender=="LSO" then -- Sender is LSO or AIRBOSS ==> Broadcast on LSO radio. self:_Number2Sound(self.LSOradio, receiver, delay) elseif sender=="MARSHAL" then @@ -6129,8 +6147,8 @@ function AIRBOSS:_AddF10Commands(_unitName) -- F10/Airboss//Help/Mark Zones/ missionCommands.addCommandForGroup(gid, "Smoke My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) missionCommands.addCommandForGroup(gid, "Flare My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) - missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCase23Zones, self, _unitName, false) - missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCase23Zones, self, _unitName, true) + missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) + missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F10/Airboss//Help/ missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) @@ -6928,37 +6946,46 @@ function AIRBOSS:_MarkMarshalZone(_unitName, flare) if playerData then - -- Get current holding zone. - local zone=self:_GetZoneHolding(playerData.case, playerData.flag:Get()) - - local text="No marshal zone to smoke!" - if zone then - - --TODO: Add height! + -- Get player stack and recovery case. + local stack=playerData.flag:Get() + local case=playerData.case + local text="" + if stack>0 then + + -- Get current holding zone. + local zone=self:_GetZoneHolding(case, stack) + + -- Pattern alitude. + local patternalt=self:_GetMarshalAltitude(stack, case) + + patternalt=0 + if flare then text="Marking marshal zone with WHITE flares." - zone:FlareZone(FLARECOLOR.White, 45) + zone:FlareZone(FLARECOLOR.White, 45, nil, patternalt) else text="Marking marshal zone with WHITE smoke." - zone:SmokeZone(SMOKECOLOR.White, 45) + zone:SmokeZone(SMOKECOLOR.White, 45, patternalt) end + else + text="You are currently not in a marshal stack. No zone to mark!" end -- Send message to player. - self:MessageToPlayer(playerData, text, "MARSHAL", "", 10) + self:MessageToPlayer(playerData, text, "MARSHAL") end end end ---- Mark current marshal zone of player by either smoke or flares. +--- Mark CASE I or II/II zones by either smoke or flares. -- @param #AIRBOSS self -- @param #string _unitName Name of the player unit. -- @param #boolean flare If true, flare the zone. If false, smoke the zone. -function AIRBOSS:_MarkCase23Zones(_unitName, flare) +function AIRBOSS:_MarkCaseZones(_unitName, flare) -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) @@ -6973,10 +7000,9 @@ function AIRBOSS:_MarkCase23Zones(_unitName, flare) local case=playerData.case -- Initial - local text=string.format("Marking CASE %d zone:\n", case) - - --TODO: Add height - at least in some cases? + local text=string.format("Marking CASE %d zones\n", case) + -- Flare or smoke? if flare then -- Case I/II: Initial @@ -7052,13 +7078,12 @@ function AIRBOSS:_MarkCase23Zones(_unitName, flare) end -- Send message to player. - self:MessageToPlayer(playerData, text, "AIRBOSS", "", 10) + self:MessageToPlayer(playerData, text, "MARSHAL") end end end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ From e7f1d98b6447489345229fd3c0e78b729c6d9f98 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 5 Dec 2018 23:58:37 +0100 Subject: [PATCH 079/147] AIRBOSS v0.4.7 --- Moose Development/Moose/Ops/Airboss.lua | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 7670d53ef..8d8eda79e 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -125,7 +125,7 @@ -- @field #AIRBOSS AIRBOSS = { ClassName = "AIRBOSS", - Debug = true, + Debug = false, lid = nil, carrier = nil, carriertype = nil, @@ -320,12 +320,14 @@ AIRBOSS.LSOCall={ }, HIGH={ file="LSO-High", + suffix="ogg", loud=true, subtitle="You're high", duration=0.65, }, LOW={ file="LSO-Low", + suffix="ogg", loud=true, subtitle="You're low", duration=0.50, @@ -426,7 +428,7 @@ AIRBOSS.LSOCall={ suffix="ogg", louder=false, subtitle="2", - duration=0.35, + duration=0.37, }, N3={ file="LSO-N3", @@ -519,7 +521,7 @@ AIRBOSS.MarshalCall={ suffix="ogg", louder=false, subtitle="2", - duration=0.35, + duration=0.37, }, N3={ file="LSO-N3", @@ -692,7 +694,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.6" +AIRBOSS.version="0.4.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -801,7 +803,7 @@ function AIRBOSS:New(carriername, alias) self:SetHoldingOffsetAngle(15) -- Default recovery case. - self:SetRecoveryCase(3) + self:SetRecoveryCase(1) -- CCA 50 NM radius zone around the carrier. self:SetCarrierControlledArea() @@ -2816,7 +2818,7 @@ function AIRBOSS:OnEventBirth(EventData) -- Debug. if self.Debug then self:_Number2Sound(self.LSOradio, "0123456789", 10) - self:_Number2Sound(self.Carrierradio, "0123456789", 30) + self:_Number2Sound(self.Carrierradio, "0123456789", 20) end end @@ -5914,7 +5916,7 @@ function AIRBOSS:RadioTransmit(radio, call, loud, delay) subtitle=subtitle.."." end end - filename=filename.."."..call.suffix + filename=filename.."."..(call.suffix or "ogg") -- New transmission. radio:NewUnitTransmission(filename, call.subtitle, call.duration, radio.Frequency/1000000, radio.Modulation, false) From 96b60d8ac03636b06396dc36ee2130b3b9a413ab Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 6 Dec 2018 00:05:41 +0100 Subject: [PATCH 080/147] AIRBOSS v0.4.8 --- Moose Development/Moose/Ops/Airboss.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 8d8eda79e..d0ec72d92 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -694,7 +694,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.7" +AIRBOSS.version="0.4.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -3855,7 +3855,7 @@ function AIRBOSS:_Groove(playerData) playerData.groove.IC=groovedata -- Check if player should wave off. - local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData.difficulty) + local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) -- Let's see.. if waveoff then @@ -3946,7 +3946,7 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) self:I(self.lid..string.format("%s: Wave off due to glide slope error %.1f > 1 degree!", playerData.name, glideslopeError)) waveoff=true end - + -- Too far from centerline? if math.abs(lineupError)>3 then self:I(self.lid..string.format("%s: Wave off due to line up error %.1f > 3 degrees!", playerData.name, lineupError)) From 5a327b1d6b8c99c84bfe86bfd92d9841cd57dc3a Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 6 Dec 2018 16:10:17 +0100 Subject: [PATCH 081/147] AIRBOSS v0.4.8w --- .../Moose/Functional/Artillery.lua | 2 +- Moose Development/Moose/Ops/Airboss.lua | 350 +++++++++++------- .../Moose/Ops/RecoveryTanker.lua | 94 +++-- 3 files changed, 285 insertions(+), 161 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 3c370915f..637560d03 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -2878,7 +2878,7 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) self.Controllable:ClearTasks() else - self:E(ARTY.id.."ERROR: No target in cease fire for group %s.", self.groupname) + self:E(ARTY.id..string.format("ERROR: No target in cease fire for group %s.", self.groupname)) end -- Set number of shots to zero. diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index d0ec72d92..7222b4c92 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -73,7 +73,11 @@ -- @field #AIRBOSS.Checkpoint Platform Case II/III descent at 2000 ft/min at 5000 ft platform. -- @field #AIRBOSS.Checkpoint DirtyUp Case II/III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. -- @field #AIRBOSS.Checkpoint Bullseye Case III intercept glideslope and follow ICLS aka "bullseye". --- @field #number case Recovery case I, II or III in progress. +-- @field #number defaultcase Default recovery case. This is the case used if not specified otherwise. +-- @field #number case Recovery case I, II or III currently in progress. +-- @field #table recoverytimes List of time windows when aircraft are recovered including the recovery case and holding offset. +-- @field #number defaultoffset Default holding pattern update if not specified otherwise. +-- @field #number holdingoffset Offset [degrees] of Case II/III holding pattern. -- @field #table flights List of all flights in the CCA. -- @field #table Qmarshal Queue of marshalling aircraft groups. -- @field #table Qpattern Queue of aircraft groups in the landing pattern. @@ -82,8 +86,6 @@ -- @field #number Nmaxpattern Max number of aircraft in landing pattern. -- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. -- @field Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. --- @field #table recoverytimes List of time windows when aircraft are recovered including the recovery case. --- @field #number holdingoffset Offset [degrees] of Case II/III holding pattern. Default 0 degrees. -- @extends Core.Fsm#FSM --- The boss! @@ -163,7 +165,11 @@ AIRBOSS = { Platform = {}, DirtyUp = {}, Bullseye = {}, - case = 1, + defaultcase = nil, + case = nil, + defaultoffset = nil, + holdingoffset = nil, + recoverytimes = {}, flights = {}, Qpattern = {}, Qmarshal = {}, @@ -172,8 +178,6 @@ AIRBOSS = { Nmaxpattern = nil, tanker = nil, warehouse = nil, - recoverytimes = {}, - holdingoffset = nil, } --- Player aircraft types capable of landing on carriers. @@ -235,9 +239,9 @@ AIRBOSS.CarrierType={ -- @type AIRBOSS.AircraftAoA -- @field #number OnSpeedMin Minimum on speed AoA. Values below are fast -- @field #number OnSpeedMax Maximum on speed AoA. Values above are slow. --- @field #number OnSpeed Optimal AoA. +-- @field #number OnSpeed Optimal on-speed AoA. -- @field #number Fast Fast AoA threshold. Smaller means faster. --- @field #number Slow Slow AoA threshold. Larger means slower +-- @field #number Slow Slow AoA threshold. Larger means slower. --- Pattern steps. -- @type AIRBOSS.PatternStep @@ -270,7 +274,7 @@ AIRBOSS.PatternStep={ } --- Radio sound file and subtitle. --- @type AIRBOSS.RadioSound +-- @type AIRBOSS.RadioCall -- @field #string file Sound file name without suffix. -- @field #string suffix File suffix/extention, e.g. "ogg". -- @field #boolean loud Loud version of sound file available. @@ -279,31 +283,39 @@ AIRBOSS.PatternStep={ --- LSO radio calls. -- @type AIRBOSS.LSOCall --- @field #AIRBOSS.RadioSound RIGHTFORLINEUP "Right for line up" call. --- @field #AIRBOSS.RadioSound COMELEFT "Come left" call. --- @field #AIRBOSS.RadioSound HIGH "You're high" call. --- @field #AIRBOSS.RadioSound LOW "You're low" call. --- @field #AIRBOSS.RadioSound POWER "Power" call. --- @field #AIRBOSS.RadioSound FAST "You're fast" call. --- @field #AIRBOSS.RadioSound SLOW "You're slow" call. --- @field #AIRBOSS.RadioSound PADDLESCONTACT "Paddles, contact" call. --- @field #AIRBOSS.RadioSound CALLTHEBALL "Call the Ball" --- @field #AIRBOSS.RadioSound ROGERBALL "Roger ball" call. --- @field #AIRBOSS.RadioSound WAVEOFF "Wafe off" call --- @field #AIRBOSS.RadioSound BOLTER "Bolter, Bolter" call --- @field #AIRBOSS.RadioSound LONGINGROOVE "You're long in the groove. Depart and re-enter." call. --- @field #AIRBOSS.RadioSound DEPARTANDREENTER "Depart and re-enter" call. --- @field #AIRBOSS.RadioSound N1 "One" call. --- @field #AIRBOSS.RadioSound N2 "Two" call. --- @field #AIRBOSS.RadioSound N3 "Three" call. --- @field #AIRBOSS.RadioSound N4 "Four" call. --- @field #AIRBOSS.RadioSound N5 "Five" call. --- @field #AIRBOSS.RadioSound N6 "Six" call. --- @field #AIRBOSS.RadioSound N7 "Seven" call. --- @field #AIRBOSS.RadioSound N8 "Eight" call. --- @field #AIRBOSS.RadioSound N9 "Nine" call. --- @field #AIRBOSS.RadioSound N0 "Zero" call. +-- @field #AIRBOSS.RadioCall RADIOCHECK "Paddles, radio check" call. +-- @field #AIRBOSS.RadioCall RIGHTFORLINEUP "Right for line up" call. +-- @field #AIRBOSS.RadioCall COMELEFT "Come left" call. +-- @field #AIRBOSS.RadioCall HIGH "You're high" call. +-- @field #AIRBOSS.RadioCall LOW "You're low" call. +-- @field #AIRBOSS.RadioCall POWER "Power" call. +-- @field #AIRBOSS.RadioCall FAST "You're fast" call. +-- @field #AIRBOSS.RadioCall SLOW "You're slow" call. +-- @field #AIRBOSS.RadioCall PADDLESCONTACT "Paddles, contact" call. +-- @field #AIRBOSS.RadioCall CALLTHEBALL "Call the Ball" +-- @field #AIRBOSS.RadioCall ROGERBALL "Roger ball" call. +-- @field #AIRBOSS.RadioCall WAVEOFF "Wafe off" call +-- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call +-- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove. Depart and re-enter." call. +-- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call. +-- @field #AIRBOSS.RadioCall N0 "Zero" call. +-- @field #AIRBOSS.RadioCall N1 "One" call. +-- @field #AIRBOSS.RadioCall N2 "Two" call. +-- @field #AIRBOSS.RadioCall N3 "Three" call. +-- @field #AIRBOSS.RadioCall N4 "Four" call. +-- @field #AIRBOSS.RadioCall N5 "Five" call. +-- @field #AIRBOSS.RadioCall N6 "Six" call. +-- @field #AIRBOSS.RadioCall N7 "Seven" call. +-- @field #AIRBOSS.RadioCall N8 "Eight" call. +-- @field #AIRBOSS.RadioCall N9 "Nine" call. AIRBOSS.LSOCall={ + RADIOCHECK={ + file="LSO-RadioCheck", + suffix="ogg", + louder=false, + subtitle="Paddles, radio check", + duration=1.1, + }, RIGHTFORLINEUP={ file="LSO-RightForLineup", suffix="ogg", @@ -402,13 +414,6 @@ AIRBOSS.LSOCall={ subtitle="Paddles, contact", duration=1.0, }, - RADIOCHECK={ - file="LSO-RadioCheck", - suffix="ogg", - louder=false, - subtitle="Paddles, radio check", - duration=1.1, - }, N0={ file="LSO-N0", suffix="ogg", @@ -483,16 +488,17 @@ AIRBOSS.LSOCall={ --- Marshal radio calls. -- @type AIRBOSS.MarshalCall --- @field #AIRBOSS.RadioSound N1 "One" call. --- @field #AIRBOSS.RadioSound N2 "Two" call. --- @field #AIRBOSS.RadioSound N3 "Three" call. --- @field #AIRBOSS.RadioSound N4 "Four" call. --- @field #AIRBOSS.RadioSound N5 "Five" call. --- @field #AIRBOSS.RadioSound N6 "Six" call. --- @field #AIRBOSS.RadioSound N7 "Seven" call. --- @field #AIRBOSS.RadioSound N8 "Eight" call. --- @field #AIRBOSS.RadioSound N9 "Nine" call. --- @field #AIRBOSS.RadioSound N0 "Zero" call. +-- @field #AIRBOSS.RadioCall RADIOCHECK "Marshal, radio check" call. +-- @field #AIRBOSS.RadioCall N0 "Zero" call. +-- @field #AIRBOSS.RadioCall N1 "One" call. +-- @field #AIRBOSS.RadioCall N2 "Two" call. +-- @field #AIRBOSS.RadioCall N3 "Three" call. +-- @field #AIRBOSS.RadioCall N4 "Four" call. +-- @field #AIRBOSS.RadioCall N5 "Five" call. +-- @field #AIRBOSS.RadioCall N6 "Six" call. +-- @field #AIRBOSS.RadioCall N7 "Seven" call. +-- @field #AIRBOSS.RadioCall N8 "Eight" call. +-- @field #AIRBOSS.RadioCall N9 "Nine" call. AIRBOSS.MarshalCall={ RADIOCHECK={ file="MARSHAL-RadioCheck", @@ -585,11 +591,12 @@ AIRBOSS.Difficulty={ HARD="TOPGUN Graduate", } ---- Recovery time. +--- Recovery window parameters. -- @type AIRBOSS.Recovery -- @field #number START Start of recovery in seconds of abs time. -- @field #number STOP End of recovery in seconds of abs time. -- @field #number CASE Recovery case (1-3) of that time slot. +-- @field #number OFFSET Angle offset of the holding pattern in degrees. Usually 0, +-15, or +-30 degrees. --- Groove position. -- @type AIRBOSS.GroovePos @@ -641,11 +648,6 @@ AIRBOSS.GroovePos={ -- @field #number LimitXmax Latitudal threshold for triggering the next step if X>Xmax. -- @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. --- Parameters of a flight group. -- @type AIRBOSS.Flightitem @@ -694,7 +696,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.8" +AIRBOSS.version="0.4.8w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -706,7 +708,7 @@ AIRBOSS.version="0.4.8" -- TODO: Option to turn AI handling off. -- TODO: Check distance to players during approach. PWO if too close. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! --- TODO: Add radio check (LSO, AIRBOSS) to F10 radio menu. +-- DONE: Add radio check (LSO, AIRBOSS) to F10 radio menu. -- TODO: Add user functions. -- TODO: Generalize parameters for other carriers. -- TODO: Generalize parameters for other aircraft. @@ -799,11 +801,11 @@ function AIRBOSS:New(carriername, alias) -- Set max aircraft in landing pattern. self:SetMaxLandingPattern(2) - -- Set holding offset to 0 degrees. - self:SetHoldingOffsetAngle(15) - - -- Default recovery case. + -- Default recovery case. This sets self.defaultcase and self.case. self:SetRecoveryCase(1) + + -- Set holding offset to 0 degrees. This set self.defaultoffset and self.holdingoffset. + self:SetHoldingOffsetAngle(15) -- CCA 50 NM radius zone around the carrier. self:SetCarrierControlledArea() @@ -845,7 +847,7 @@ function AIRBOSS:New(carriername, alias) --[[ -- Init default sound files. for _name,_sound in pairs(AIRBOSS.LSOCall) do - local sound=_sound --#AIRBOSS.RadioSound + local sound=_sound --#AIRBOSS.RadioCall local text=string.format() sound.subtitle=1 sound.louder=1 @@ -901,12 +903,14 @@ function AIRBOSS:New(carriername, alias) -- @function [parent=#AIRBOSS] RecoveryStart -- @param #AIRBOSS self -- @param #number Case Recovery case (1, 2 or 3) that is started. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. --- Triggers the FSM delayed event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern. -- @function [parent=#AIRBOSS] __RecoveryStart + -- @param #number delay Delay in seconds. -- @param #AIRBOSS self -- @param #number Case Recovery case (1, 2 or 3) that is started. - -- @param #number delay Delay in seconds. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. --- Triggers the FSM event "RecoveryStop" that stops the recovery of aircraft. @@ -923,12 +927,14 @@ function AIRBOSS:New(carriername, alias) -- @function [parent=#AIRBOSS] RecoveryCase -- @param #AIRBOSS self -- @param #number Case The new recovery case (1, 2 or 3). + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. --- Triggers the delayed FSM event "RecoveryCase" that sets the used aircraft recovery case. -- @function [parent=#AIRBOSS] __Case -- @param #AIRBOSS self -- @param #number delay Delay in seconds. -- @param #number Case The new recovery case (1, 2 or 3). + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. @@ -975,14 +981,16 @@ function AIRBOSS:SetCarrierControlledZone(radius) return self end ---- Set recovery case pattern. +--- Set the default recovery case. -- @param #AIRBOSS self --- @param #number case Case of recovery. Either 1 or 3. Default 1. +-- @param #number case Case of recovery. Either 1, 2 or 3. Default 1. -- @return #AIRBOSS self function AIRBOSS:SetRecoveryCase(case) - self.case=case or 1 - + self.defaultcase=case or 1 + + self.case=self.defaultcase + return self end @@ -993,8 +1001,11 @@ end -- @return #AIRBOSS self function AIRBOSS:SetHoldingOffsetAngle(offset) - self.holdingoffset=offset or 0 + self.defaultoffset=offset or 0 + + self.holdingoffset=self.defaultoffset + return self end @@ -1002,9 +1013,10 @@ end -- @param #AIRBOSS self -- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now. -- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. --- @param #number case Recovery case for that time slot. Number between one and three. Default 1. +-- @param #number case Recovery case for that time slot. Number between one and three. +-- @param #number holdingoffset Only for CASE II/III: Angle in degrees the holding pattern is offset. -- @return #AIRBOSS self -function AIRBOSS:AddRecoveryTime(starttime, stoptime, case) +function AIRBOSS:AddRecoveryTime(starttime, stoptime, case, holdingoffset) -- Set start time. local Tstart=UTILS.ClockToSeconds(starttime or UTILS.SecondsToClock(timer.getAbsTime())) @@ -1018,17 +1030,22 @@ function AIRBOSS:AddRecoveryTime(starttime, stoptime, case) return self end - -- Default is Case 1 recovery. - case=case or 1 + -- Case or default value. + case=case or self.defaultcase + + -- Holding offset or default value. + holdingoffset=holdingoffset or self.defaultoffset + -- Recovery window. - local rtime={} --#AIRBOSS.Recovery - rtime.START=Tstart - rtime.STOP=Tstop - rtime.CASE=case + local recovery={} --#AIRBOSS.Recovery + recovery.START=Tstart + recovery.STOP=Tstop + recovery.CASE=case + recovery.OFFSET=holdingoffset -- Add to table - table.insert(self.recoverytimes, rtime) + table.insert(self.recoverytimes, recovery) return self end @@ -1066,7 +1083,7 @@ end --- Set ICLS channel of carrier. -- @param #AIRBOSS self -- @param #number channel ICLS channel. Default 1. --- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". +-- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". Default "STN". -- @return #AIRBOSS self function AIRBOSS:SetICLS(channel, morsecode) @@ -1262,6 +1279,13 @@ function AIRBOSS:_CheckRecoveryTimes() text=" none!" end + -- Sort windows wrt to start time. + local _sort=function(a, b) return a.START1 then + text=text..string.format(" Holding offset angle %d degrees.", Offset) + end + MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) + self:I(self.lid..text) -- Set new recovery case. self.case=Case + + -- Set holding offset. + self.holdingoffset=Offset end --- On after "RecoveryStart" event. Recovery of aircraft is started and carrier switches to state "Recovering". @@ -1339,14 +1398,26 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param #number Case The recovery case (1, 2 or 3) to start. -function AIRBOSS:onafterRecoveryStart(From, Event, To, Case) +-- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. +function AIRBOSS:onafterRecoveryStart(From, Event, To, Case, Offset) + + -- Input or default value. + Case=Case or self.defaultcase + + -- Input or default value. + Offset=Offset or self.defaultoffset -- Debug output. - self:I(self.lid..string.format("Starting aircraft recovery in case %d.", Case)) + local text=string.format("Starting aircraft recovery case %d.", Case) + if Case>1 then + text=text..string.format(" Holding offset angle %d degrees.", Offset) + end + MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) + self:I(self.lid..text) -- Switch to case. - self:RecoveryCase(Case) - + self:RecoveryCase(Case, Offset) + end --- On after "RecoveryStop" event. Recovery of aircraft is stopped and carrier switches to state "Idle". @@ -1357,10 +1428,7 @@ end function AIRBOSS:onafterRecoveryStop(From, Event, To) -- Debug output. - self:I(self.lid..string.format("Stopping aircraft recovery.")) - - -- Switch to idle state. - self:Idle() + self:I(self.lid..string.format("Stopping aircraft recovery. Carrier goes to state idle.")) end @@ -1741,11 +1809,15 @@ function AIRBOSS:_CheckQueue() -- Time (last) flight has entered landing pattern. local Tpattern=9999 local npunits=1 + local pcase=1 if npattern>0 then -- Last flight group send to pattern. local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.Flightitem + -- Recovery case of pattern flight. + pcase=patternflight.case + -- Number of aircraft in this group. local npunits=patternflight.nunits @@ -1756,7 +1828,7 @@ function AIRBOSS:_CheckQueue() -- Min time in pattern before next aircraft is allowed. local TpatternMin - if self.case==1 then + if pcase==1 then TpatternMin=45*npunits -- 45 seconds interval per plane! else TpatternMin=120*npunits -- 120 seconds interval per plane! @@ -1781,6 +1853,7 @@ function AIRBOSS:_ScanCarrierZone() -- Carrier position. local coord=self:GetCoordinate() + -- Scan radius. local Rout=UTILS.NMToMeters(50) -- Scan units in carrier zone. @@ -1890,12 +1963,9 @@ function AIRBOSS:_MarshalPlayer(playerData) -- Check if flight is known to the airboss already. if playerData then - - -- Number of flight groups in stack. - local ngroups, nunits=self:_GetQueueInfo(self.Qmarshal, self.case) - - -- Assign next free stack to this flight. - local mystack=ngroups+1 + + -- Get free stack. + local mystack=self:_GetFreeStack(self.case) -- Add group to marshal stack. self:_AddMarshalGroup(playerData, mystack) @@ -1963,13 +2033,15 @@ function AIRBOSS:_MarshalAI(flight, nstack) -- Set up waypoints including collapsing the stack. for stack=nstack, 1, -1 do + -- TODO: skip stack 6 if recoverytanker (or at whatever angels the tanker orbits). + -- Get altitude and positions. local Altitude, p1, p2=self:_GetMarshalAltitude(stack) local p1=p1 --Core.Point#COORDINATE local Dist=p1:Get2DDistance(self:GetCoordinate()) - -- Orbit task. + -- Task: orbit at specified position, altitude and speed until flag=stack-1 local TaskOrbit=_taskorbit(p1, Altitude, Speed, stack-1, p2) -- Waypoint description. @@ -1981,7 +2053,7 @@ function AIRBOSS:_MarshalAI(flight, nstack) end -- Landing waypoint. - wp[#wp+1]=Carrier:WaypointAirLanding(Speed, self.airbase, nil, "Landing") + wp[#wp+1]=Carrier:SetAltitude(250):WaypointAirLanding(Speed, self.airbase, nil, "Landing") -- Reinit waypoints. group:WayPointInitialize(wp) @@ -2112,7 +2184,7 @@ function AIRBOSS:_CheckCollapseMarshalStack(flight) -- Hint for easy skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then - self:MessageToPlayer(flight, string.format("Use F10 radio menu \"Commence!\" command when you are ready!"), nil, "", 5) + self:MessageToPlayer(flight, string.format("Use F10 radio menu \"Request Commence\" command when ready!"), nil, "", 5) end end @@ -2147,6 +2219,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) if stack>0 and mstack>stack then -- Decrease stack/flag by one ==> AI will go lower. + -- TODO: If we include the recovery tanker, this needs to be generalized. mflight.flag:Set(mstack-1) -- Inform players. @@ -2216,16 +2289,32 @@ function AIRBOSS:_GetFreeStack(case) case=case or self.case -- Get stack - local stack + local nfull if case==1 then -- Lowest Case I stack. - stack=self:_GetQueueInfo(self.Qmarshal, 1) + nfull=self:_GetQueueInfo(self.Qmarshal, 1) else -- Lowest Case II or III stack. - stack=self:_GetQueueInfo(self.Qmarshal, 23) + nfull=self:_GetQueueInfo(self.Qmarshal, 23) end - return stack+1 + -- Get recovery tanker stack. + local tankerstack=9999 + if self.tanker and case==1 then + tankerstack=self:_GetAngels(self.tanker.altitude) + end + + local nfree + if nfull1 then -- "You're high!" self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.HIGH, true) + advice=advice+AIRBOSS.LSOCall.HIGH.duration elseif glideslopeError>0.5 then -- "You're a little high." self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.HIGH, false) + advice=advice+AIRBOSS.LSOCall.HIGH.duration elseif glideslopeError<-1.0 then -- "Power!" self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.POWER, true) + advice=advice+AIRBOSS.LSOCall.POWER.duration elseif glideslopeError<-0.5 then -- "You're a little low." self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.POWER, false) + advice=advice+AIRBOSS.LSOCall.POWER.duration else text="Good altitude." end @@ -4774,15 +4866,19 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) if lineupError<-3 then -- "Come left!" self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.COMELEFT, true) + advice=advice+AIRBOSS.LSOCall.COMELEFT.duration elseif lineupError<-1 then -- "Come left." - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.COMELEFT, false) + self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.COMELEFT, false) + advice=advice+AIRBOSS.LSOCall.COMELEFT.duration elseif lineupError>3 then -- "Right for lineup!" - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.RIGHTFORLINEUP, true) + self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.RIGHTFORLINEUP, true) + advice=advice+AIRBOSS.LSOCall.RIGHTFORLINEUP.duration elseif lineupError>1 then -- "Right for lineup." self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.RIGHTFORLINEUP, false) + advice=advice+AIRBOSS.LSOCall.RIGHTFORLINEUP.duration else text=text.."Good lineup." end @@ -4798,18 +4894,22 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- Rate aoa. if aoa>=aircraftaoa.Slow then -- "Your're slow!" - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.SLOW, true) + self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.SLOW, true) + advice=advice+AIRBOSS.LSOCall.SLOW.duration elseif aoa>=aircraftaoa.OnSpeedMax and aoa=aircraftaoa.OnSpeedMin and aoa=aircraftaoa.Fast and aoabadscore then @@ -5613,15 +5713,13 @@ function AIRBOSS:_GetFuelState(unit) return UTILS.kg2lbs(fuelstate) end ---- Get altitude in angels. +--- Convert altitude from meters to angels (thousands of feet). -- @param #AIRBOSS self --- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. --- @return #number Altitude of unit in Anglels = thouthands of feet. -function AIRBOSS:_GetAngels(unit) +-- @param alt Alitude in meters. +-- @return #number Altitude in Anglels = thousands of feet using math.floor(). +function AIRBOSS:_GetAngels(alt) - local alt=unit:GetAltitude() - - local angels=math.floor(UTILS.MetersToFeet(alt))/1000 + local angels=math.floor(UTILS.MetersToFeet(alt)/1000) return angels end @@ -5777,7 +5875,7 @@ end -- @field #number prio Priority 0-100. -- @field #boolean isplaying Currently playing. -- @field Core.Beacon#RADIO radio Radio object. --- @field #AIRBOSS.RadioSound call Radio sound. +-- @field #AIRBOSS.RadioCall call Radio sound. --- Check radio queue for transmissions to be broadcasted. -- @param #AIRBOSS self @@ -5862,7 +5960,7 @@ end --- Add Radio transmission to radio queue -- @param #AIRBOSS self -- @param Core.Radio#RADIO radio sending transmission. --- @param #AIRBOSS.RadioSound call Radio sound files and subtitles. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. -- @param #boolean loud If true, play loud sound file version. -- @param #number delay Delay in seconds, before the message is broadcasted. function AIRBOSS:RadioTransmission(radio, call, loud, delay) @@ -5893,7 +5991,7 @@ end --- Transmission radio message. -- @param #AIRBOSS self -- @param Core.Radio#RADIO radio sending transmission. --- @param #AIRBOSS.RadioSound call Radio sound files and subtitles. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. -- @param #boolean loud If true, play loud sound file version. -- @param #number delay Delay in seconds, before the message is broadcasted. function AIRBOSS:RadioTransmit(radio, call, loud, delay) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index e6babf046..827f24e1f 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -36,7 +36,8 @@ -- @field #number distStern Race-track distance astern. -- @field #number distBow Race-track distance bow. -- @field #number Dupdate Pattern update when carrier changes its position by more than this distance (meters). --- @field #number Hupdate Pattern update when carrier changes its heading by more than this number (degrees). +-- @field #number Hupdate Pattern update when carrier changes its heading by more than this number (degrees). +-- @field #boolean turning If true, carrier is turning. -- @field #number dTupdate Minimum time interval in seconds before the next pattern update can happen. -- @field #number Tupdate Last time the pattern was updated. -- @field #number takeoff Takeoff type (cold, hot, air). @@ -45,6 +46,7 @@ -- @field #boolean respawninair If true, tanker will always be respawned in air. This has no impact on the initial spawn setting. -- @field #boolean uncontrolledac If true, use and uncontrolled tanker group already present in the mission. -- @field DCS#Vec3 orientation Orientation of the carrier. Used to monitor changes and update the pattern if heading changes significantly. +-- @field DCS#Vec3 orientlast Orientation of the carrier for checking if carrier is currently turning. -- @field Core.Point#COORDINATE position Positon of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. -- @field Core.Zone#ZONE_UNIT zoneUpdate Moving zone relative to carrier. Each time the tanker is in this zone, its pattern is updated. -- @extends Core.Fsm#FSM @@ -197,6 +199,7 @@ RECOVERYTANKER = { dTupdate = nil, Dupdate = nil, Hupdate = nil, + turning = nil, Tupdate = nil, takeoff = nil, lowfuel = nil, @@ -204,13 +207,14 @@ RECOVERYTANKER = { respawninair = nil, uncontrolledac = nil, orientation = nil, + orientlast = nil, position = nil, zoneUpdate = nil, } --- Class version. -- @field #string version -RECOVERYTANKER.version="0.9.6" +RECOVERYTANKER.version="0.9.6w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -274,7 +278,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- Moving zone: Zone 1 NM astern the carrier with radius of 1 NM. self.zoneUpdate=ZONE_UNIT:New("Pattern Update Zone", self.carrier, UTILS.NMToMeters(1), {dx=-UTILS.NMToMeters(1), dy=0, relative_to_unit=true}) - self.zoneUpdate:SmokeZone(SMOKECOLOR.White, 45) + --self.zoneUpdate:SmokeZone(SMOKECOLOR.White, 45) ----------------------- --- FSM Transitions --- @@ -691,7 +695,9 @@ function RECOVERYTANKER:onafterStart(From, Event, To) -- Get initial orientation and position of carrier. self.orientation=self.carrier:GetOrientationX() + self.orientlast=self.carrier:GetOrientationX() self.position=self.carrier:GetCoordinate() + self.turning=false -- Init status updates in 10 seconds. self:__Status(10) @@ -1098,45 +1104,65 @@ function RECOVERYTANKER:_CheckPatternUpdate(dt) -- Assume no update necessary. local update=false + + local Hchange=false + local Dchange=false + local turning=false -- Get current position and orientation of carrier. local pos=self.carrier:GetCoordinate() - local vC=self.carrier:GetOrientationX() + local vNew=self.carrier:GetOrientationX() - -- Check if tanker is running and last updated is more than 10 minutes ago. - if self:IsRunning() and dt>self.dTupdate then - -- Last saved orientation of carrier. - local vP=self.orientation - - -- We only need the X-Z plane. - vC.y=0 ; vP.y=0 - - -- Get angle between the two orientation vectors in rad. - local rhdg=math.deg(math.acos(UTILS.VecDot(vC,vP)/UTILS.VecNorm(vC)/UTILS.VecNorm(vP))) + -- Reference orientation of carrier after the last update + local vOld=self.orientation - -- Check if orientation changed. - if math.abs(rhdg)>self.Hupdate then - self:T(string.format("Carrier heading changed by %d degrees. Updating recovery tanker pattern.", rhdg)) - update=true - end - - -- Get distance to saved position. - local dist=pos:Get2DDistance(self.position) - - -- Check if carrier moved more than ~10 km. - if dist>self.Dupdate then - self:T(string.format("Carrier position changed by %.1f km. Updating recovery tanker pattern.", dist/1000)) - update=true - end - + -- Last orientation from 30 seconds ago. + local vLast=self.orientlast + + -- We only need the X-Z plane. + vNew.y=0 ; vOld.y=0 + + -- Get angle between old and new orientation vectors in rad and convert to degrees. + local deltaHeading=math.deg(math.acos(UTILS.VecDot(vNew,vOld)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vOld))) + + -- Angle between current heading and last time we checked ~30 seconds ago. + local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) + + -- Last orientation becomes new orientation + self.orientlast=vNew + + -- Carrier is turning + local turning=deltaLast>=1 + + -- Check if orientation changed. + if math.abs(deltaHeading)>self.Hupdate then + self:T(string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) + Hchange=true end - -- If pattern is updated then update orientation AND positon. - -- But only if last update is less then 10 minutes ago. - if update then - self.orientation=vC - self.position=pos + -- Get distance to saved position. + local dist=pos:Get2DDistance(self.position) + + -- Check if carrier moved more than ~10 km. + if dist>self.Dupdate then + self:T(string.format("Carrier position changed by %.1f km. Turning=%s.", dist/1000, tostring(turning))) + Dchange=true + end + + -- Assume no update necessary. + local update=false + + -- No update if currently turning! Also must be running (nor RTB or refuelling) and T>~10 min. + if self:IsRunning() and dt>self.dTupdate and not turning then + + -- Update if heading or distance changed. + if Hchange or Dchange then + self.orientation=vNew + self.position=pos + update=true + end + end return update From 01b2f238c50fdb299572a8504a91e2550274a13a Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 6 Dec 2018 23:36:37 +0100 Subject: [PATCH 082/147] AIRBOSS v0.4.9 RECOVERYTANKER v0.9.7 --- Moose Development/Moose/Core/Radio.lua | 16 +-- Moose Development/Moose/Ops/Airboss.lua | 126 ++++++++++-------- .../Moose/Ops/RecoveryTanker.lua | 108 ++++++++------- Moose Development/Moose/Utilities/Utils.lua | 26 ++-- 4 files changed, 156 insertions(+), 120 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index f01cb9a09..5656ffc40 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -172,9 +172,7 @@ function RADIO:SetFrequency(Frequency) -- Convert frequency from MHz to Hz self.Frequency = Frequency * 1000000 - - - + -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then @@ -186,7 +184,7 @@ function RADIO:SetFrequency(Frequency) } } - self:I(commandSetFrequency) + self:T2(commandSetFrequency) self.Positionable:SetCommand(commandSetFrequency) end @@ -355,7 +353,7 @@ function RADIO:Broadcast(viatrigger) -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system. if (self.Positionable.ClassName=="UNIT" or self.Positionable.ClassName=="GROUP") and (not viatrigger) then - self:I("Broadcasting from a UNIT or a GROUP") + self:T("Broadcasting from a UNIT or a GROUP") local commandTransmitMessage={ id = "TransmitMessage", @@ -366,12 +364,12 @@ function RADIO:Broadcast(viatrigger) loop = self.Loop, }} - self:I(commandTransmitMessage) + self:T3(commandTransmitMessage) self.Positionable:SetCommand(commandTransmitMessage) else -- If the POSITIONABLE is anything else, we revert to the general singleton function -- I need to give it a unique name, so that the transmission can be stopped later. I use the class ID - self:I("Broadcasting from a POSITIONABLE") + self:T("Broadcasting from a POSITIONABLE") trigger.action.radioTransmission(self.FileName, self.Positionable:GetPositionVec3(), self.Modulation, self.Loop, self.Frequency, self.Power, tostring(self.ID)) end @@ -522,7 +520,7 @@ end -- -- myBeacon:TACAN(20, "Y", "TEXACO", true) -- Activate the beacon function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) - self:I({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) + self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) -- Get frequency. local Frequency=UTILS.TACANToFrequency(Channel, Mode) @@ -648,7 +646,7 @@ function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) }) if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD - SCHEDULER:New( nil, + SCHEDULER:New(nil, function() self:StopAATACAN() end, {}, BeaconDuration) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 7222b4c92..2835b5165 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -294,7 +294,7 @@ AIRBOSS.PatternStep={ -- @field #AIRBOSS.RadioCall PADDLESCONTACT "Paddles, contact" call. -- @field #AIRBOSS.RadioCall CALLTHEBALL "Call the Ball" -- @field #AIRBOSS.RadioCall ROGERBALL "Roger ball" call. --- @field #AIRBOSS.RadioCall WAVEOFF "Wafe off" call +-- @field #AIRBOSS.RadioCall WAVEOFF "Wave off" call -- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call -- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove. Depart and re-enter." call. -- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call. @@ -312,7 +312,7 @@ AIRBOSS.LSOCall={ RADIOCHECK={ file="LSO-RadioCheck", suffix="ogg", - louder=false, + loud=false, subtitle="Paddles, radio check", duration=1.1, }, @@ -349,7 +349,7 @@ AIRBOSS.LSOCall={ suffix="ogg", loud=true, subtitle="Power", - duration=0.45, + duration=0.50, --0.45 was too short }, SLOW={ file="LSO-Slow", @@ -368,121 +368,121 @@ AIRBOSS.LSOCall={ CALLTHEBALL={ file="LSO-CallTheBall", suffix="ogg", - louder=false, + loud=false, subtitle="Call the ball", duration=0.6, }, ROGERBALL={ file="LSO-RogerBall", suffix="ogg", - louder=false, + loud=false, subtitle="Roger ball", duration=0.7, }, WAVEOFF={ file="LSO-WaveOff", suffix="ogg", - louder=false, + loud=false, subtitle="Wave off", duration=0.6, }, BOLTER={ file="LSO-BolterBolter", suffix="ogg", - louder=false, - subtitle="Bolter, Bolter!", + loud=false, + subtitle="Bolter, Bolter", duration=0.75, }, LONGINGROOVE={ file="LSO-LongInTheGroove", suffix="ogg", - louder=false, + loud=false, subtitle="You're long in the groove", duration=1.2, }, DEPARTANDREENTER={ file="LSO-DepartAndReenter", suffix="ogg", - louder=false, + loud=false, subtitle="Depart and re-enter", duration=1.1, }, PADDLESCONTACT={ file="LSO-PaddlesContact", suffix="ogg", - louder=false, + loud=false, subtitle="Paddles, contact", duration=1.0, }, N0={ file="LSO-N0", suffix="ogg", - louder=false, - subtitle="0", + loud=false, + subtitle="", duration=0.40, }, N1={ file="LSO-N1", suffix="ogg", - louder=false, - subtitle="1", + loud=false, + subtitle="", duration=0.25, }, N2={ file="LSO-N2", suffix="ogg", - louder=false, - subtitle="2", + loud=false, + subtitle="", duration=0.37, }, N3={ file="LSO-N3", suffix="ogg", - louder=false, - subtitle="3", + loud=false, + subtitle="", duration=0.37, }, N4={ file="LSO-N4", suffix="ogg", - louder=false, - subtitle="4", + loud=false, + subtitle="", duration=0.39, }, N5={ file="LSO-N5", suffix="ogg", - louder=false, - subtitle="5", + loud=false, + subtitle="", duration=0.38, }, N6={ file="LSO-N6", suffix="ogg", - louder=false, - subtitle="6", + loud=false, + subtitle="", duration=0.40, }, N7={ file="LSO-N7", suffix="ogg", - louder=false, - subtitle="7", + loud=false, + subtitle="", duration=0.40, }, N8={ file="LSO-N8", suffix="ogg", - louder=false, - subtitle="8", + loud=false, + subtitle="", duration=0.37, }, N9={ file="LSO-N9", suffix="ogg", - louder=false, - subtitle="9", - duration=0.38, + loud=false, + subtitle="", + duration=0.40, --0.38 too short }, } @@ -503,7 +503,7 @@ AIRBOSS.MarshalCall={ RADIOCHECK={ file="MARSHAL-RadioCheck", suffix="ogg", - louder=false, + loud=false, subtitle="Marshal, radio check", duration=1.0, }, @@ -511,72 +511,72 @@ AIRBOSS.MarshalCall={ N0={ file="LSO-N0", suffix="ogg", - louder=false, + loud=false, subtitle="0", duration=0.40, }, N1={ file="LSO-N1", suffix="ogg", - louder=false, + loud=false, subtitle="1", duration=0.25, }, N2={ file="LSO-N2", suffix="ogg", - louder=false, + loud=false, subtitle="2", duration=0.37, }, N3={ file="LSO-N3", suffix="ogg", - louder=false, + loud=false, subtitle="3", duration=0.37, }, N4={ file="LSO-N4", suffix="ogg", - louder=false, + loud=false, subtitle="4", duration=0.39, }, N5={ file="LSO-N5", suffix="ogg", - louder=false, + loud=false, subtitle="5", duration=0.38, }, N6={ file="LSO-N6", suffix="ogg", - louder=false, + loud=false, subtitle="6", duration=0.40, }, N7={ file="LSO-N7", suffix="ogg", - louder=false, + loud=false, subtitle="7", duration=0.40, }, N8={ file="LSO-N8", suffix="ogg", - louder=false, + loud=false, subtitle="8", duration=0.37, }, N9={ file="LSO-N9", suffix="ogg", - louder=false, + loud=false, subtitle="9", - duration=0.38, + duration=0.40, --0.38 too short }, } @@ -696,7 +696,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.8w" +AIRBOSS.version="0.4.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -708,12 +708,12 @@ AIRBOSS.version="0.4.8w" -- TODO: Option to turn AI handling off. -- TODO: Check distance to players during approach. PWO if too close. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! --- DONE: Add radio check (LSO, AIRBOSS) to F10 radio menu. -- TODO: Add user functions. -- TODO: Generalize parameters for other carriers. -- TODO: Generalize parameters for other aircraft. -- TODO: Foul deck check. -- TODO: Persistence of results. +-- DONE: Add radio check (LSO, AIRBOSS) to F10 radio menu. -- DONE: Right pattern step after bolter/wo/patternWO? Guess so. -- DONE: Set case II and III times (via recovery time). -- DONE: Get correct wire when trapped. DONE but might need further tweaking. @@ -843,23 +843,39 @@ function AIRBOSS:New(carriername, alias) self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) end - ---[[ + + -- If calls should be part of self and individual for different carriers. + --[[ -- Init default sound files. for _name,_sound in pairs(AIRBOSS.LSOCall) do local sound=_sound --#AIRBOSS.RadioCall local text=string.format() sound.subtitle=1 - sound.louder=1 + sound.loud=1 --self.radiocall[_name]=sound end + ]] -- Debug: - self:T(self.lid.."Default sound files:") - for _name,_sound in pairs(self.radiocall) do - self:T{name=_name,sound=_sound} + if false then + local text="Playing default sound files:" + for _name,_call in pairs(AIRBOSS.LSOCall) do + local call=_call --#AIRBOSS.RadioCall + + -- Debug text. + text=text..string.format("\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring(call.loud), call.subtitle) + + -- Radio transmission to queue. + self:RadioTransmission(self.LSOradio, call, false, 10) + + -- Also play the loud version. + if call.loud then + self:RadioTransmission(self.LSOradio, call, true, 10) + end + end + self:I(self.lid..text) end -]] + ----------------------- --- FSM Transitions --- @@ -5875,7 +5891,8 @@ end -- @field #number prio Priority 0-100. -- @field #boolean isplaying Currently playing. -- @field Core.Beacon#RADIO radio Radio object. --- @field #AIRBOSS.RadioCall call Radio sound. +-- @field #AIRBOSS.RadioCall call Radio call. +-- @field #boolean loud If true, play loud version of file. --- Check radio queue for transmissions to be broadcasted. -- @param #AIRBOSS self @@ -5975,6 +5992,7 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay) transmission.prio=50 transmission.isplaying=false transmission.Tstarted=nil + transmission.loud=loud and call.loud -- Add transmission to the right queue. if radio:GetAlias()=="LSO" then diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 827f24e1f..6302c5537 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -28,7 +28,7 @@ -- @field Wrapper.Airbase#AIRBASE airbase The home airbase object of the tanker. Normally the aircraft carrier. -- @field Core.Radio#BEACON beacon Tanker TACAN beacon. -- @field #number TACANchannel TACAN channel. Default 1. --- @field #string TACANmode TACAN mode, i.e. "X" or "Y". Default "Y". +-- @field #string TACANmode TACAN mode, i.e. "X" or "Y". Default "Y". Use only "Y" for AA TACAN stations! -- @field #string TACANmorse TACAN morse code. Three letters identifying the TACAN station. Default "TKR". -- @field #boolean TACANon If true, TACAN is automatically activated. If false, TACAN is disabled. -- @field #number speed Tanker speed when flying pattern. @@ -37,7 +37,6 @@ -- @field #number distBow Race-track distance bow. -- @field #number Dupdate Pattern update when carrier changes its position by more than this distance (meters). -- @field #number Hupdate Pattern update when carrier changes its heading by more than this number (degrees). --- @field #boolean turning If true, carrier is turning. -- @field #number dTupdate Minimum time interval in seconds before the next pattern update can happen. -- @field #number Tupdate Last time the pattern was updated. -- @field #number takeoff Takeoff type (cold, hot, air). @@ -123,18 +122,18 @@ -- -- A TACAN beacon for the tanker can be activated via scripting, i.e. no need to do this within the mission editor. -- --- The beacon is create with the @{#RECOVERYTANKER.SetTACAN}(*channel*, *mode*, *morse*) function, where *channel* is the TACAN channel (a number), *mode* the TACAN mode (either "X" --- or "Y") and *morse* a three letter string that is send as morse code to identify the tanker: +-- The beacon is create with the @{#RECOVERYTANKER.SetTACAN}(*channel*, *morse*) function, where *channel* is the TACAN channel (a number), +-- and *morse* a three letter string that is send as morse code to identify the tanker: -- --- TexacoStennis:SetTACAN(10, "Y", "TKR") +-- TexacoStennis:SetTACAN(10, "TKR") -- -- will activate a TACAN beacon 10Y with more code "TKR". -- --- If you do not set a TACAN beacon explicitly, it is automatically create on channel 1, mode "Y" and morse code "TKR". +-- If you do not set a TACAN beacon explicitly, it is automatically create on channel 1Y and morse code "TKR". +-- The mode is *always* "Y" for AA TACAN stations since mode "X" does not work! -- -- In order to completely disable the TACAN beacon, you can use the @{#RECOVERYTANKER.SetTACANoff}() function in your script. --- --- Note to self, I am not sure, if an AA TACAN station *must* be of mode "Y" in order to work. It seems that this was the case in earlier DCS versions. +-- -- -- ## Pattern Update -- @@ -146,7 +145,8 @@ -- **Note** that updating the pattern always leads to a small disruption in the perfect racetrack pattern of the tanker. This is because a new waypoint and new racetrack points -- need to be set as DCS task. This is also the reason why the pattern is not contantly updated but rather when the position or heading of the carrier changes significantly. -- --- The maximum update frequency is set to 15 minutes. You can adjust this by @{#RECOVERYTANKER.SetPatternUpdateInterval}. +-- The maximum update frequency is set to 10 minutes. You can adjust this by @{#RECOVERYTANKER.SetPatternUpdateInterval}. +-- Also the pattern will not be updated while the carrier is turning or the tanker is currently refuelling another unit. -- -- # Finite State Model -- @@ -181,7 +181,7 @@ -- @field #RECOVERYTANKER RECOVERYTANKER = { ClassName = "RECOVERYTANKER", - Debug = true, + Debug = false, carrier = nil, carriertype = nil, tankergroupname = nil, @@ -199,7 +199,6 @@ RECOVERYTANKER = { dTupdate = nil, Dupdate = nil, Hupdate = nil, - turning = nil, Tupdate = nil, takeoff = nil, lowfuel = nil, @@ -214,16 +213,16 @@ RECOVERYTANKER = { --- Class version. -- @field #string version -RECOVERYTANKER.version="0.9.6w" +RECOVERYTANKER.version="0.9.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Seamless change of position update. Get good updated waypoint and update position if tanker position is right! --- TODO: Check if TACAN mode "X" is allowed for AA TACAN stations. --- TODO: Check if tanker is going back to "Running" state after RTB and respawn. -- TODO: Is alive check for tanker necessary? +-- DONE: Check if TACAN mode "X" is allowed for AA TACAN stations. Nope +-- DONE: Check if tanker is going back to "Running" state after RTB and respawn. -- DONE: Write documenation. -- DONE: Trace functions self:T instead of self:I for less output. -- DONE: Make pattern update parameters (distance, orientation) input parameters. @@ -267,7 +266,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetSpeed() self:SetRacetrackDistances(6, 8) self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) - self:SetTakeoffAir() + self:SetTakeoffHot() self:SetLowFuelThreshold() self:SetRespawnOnOff() self:SetTACAN() @@ -276,8 +275,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetPatternUpdateInterval() -- Moving zone: Zone 1 NM astern the carrier with radius of 1 NM. - self.zoneUpdate=ZONE_UNIT:New("Pattern Update Zone", self.carrier, UTILS.NMToMeters(1), {dx=-UTILS.NMToMeters(1), dy=0, relative_to_unit=true}) - + self.zoneUpdate=ZONE_UNIT:New("Pattern Update Zone", self.carrier, UTILS.NMToMeters(1), {dx=-UTILS.NMToMeters(1), dy=0, relative_to_unit=true}) --self.zoneUpdate:SmokeZone(SMOKECOLOR.White, 45) ----------------------- @@ -441,10 +439,10 @@ end --- Set minimum pattern update interval. After a pattern update this time interval has to pass before the next update is allowed. -- @param #RECOVERYTANKER self --- @param #number interval Min interval in minutes. Default is 15 minutes. +-- @param #number interval Min interval in minutes. Default is 10 minutes. -- @return #RECOVERYTANKER self function RECOVERYTANKER:SetPatternUpdateInterval(interval) - self.dTupdate=(interval or 15)*60 + self.dTupdate=(interval or 10)*60 return self end @@ -575,20 +573,35 @@ function RECOVERYTANKER:SetTACANoff() return self end ---- Set TACAN channel of tanker. +--- Set TACAN channel of tanker. Note that mode is automatically set to "Y" for AA TACAN since only that works. -- @param #RECOVERYTANKER self -- @param #number channel TACAN channel. Default 1. --- @param #string mode TACAN mode, i.e. "X" or "Y". Default "Y". -- @param #string morse TACAN morse code identifier. Three letters. Default "TKR". -- @return #RECOVERYTANKER self -function RECOVERYTANKER:SetTACAN(channel, mode, morse) +function RECOVERYTANKER:SetTACAN(channel, morse) self.TACANchannel=channel or 1 - self.TACANmode=mode or "Y" + self.TACANmode="Y" self.TACANmorse=morse or "TKR" self.TACANon=true return self end +--- Activate debug mode. Marks of pattern on F10 map etc. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetDebugModeON() + self.Debug=true + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetDebugModeOFF() + self.Debug=false + return self +end + --- Check if tanker is currently returning to base. -- @param #RECOVERYTANKER self -- @return #boolean If true, tanker is returning to base. @@ -697,7 +710,6 @@ function RECOVERYTANKER:onafterStart(From, Event, To) self.orientation=self.carrier:GetOrientationX() self.orientlast=self.carrier:GetOrientationX() self.position=self.carrier:GetCoordinate() - self.turning=false -- Init status updates in 10 seconds. self:__Status(10) @@ -717,15 +729,17 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) -- Get fuel of tanker. local fuel=self.tanker:GetFuel()*100 local text=string.format("Recovery tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) - self:I(text) - + self:T(text) + -- Check if tanker flies through pattern update zone. -- TODO: Check if this can be used to update the pattern without too much disruption. - -- Could be a problem when carrier changes course since the tanker might not fligh through the zone any more. - local inupdatezone=self.tanker:GetUnit(1):IsInZone(self.zoneUpdate) - if inupdatezone then - local clock=UTILS.SecondsToClock(timer.getAbsTime()) - self:I(string.format("Recovery tanker is in pattern update zone! Time=%s", clock)) + -- Could be a problem when carrier changes course since the tanker might not fligh through the zone any more. + if self.Debug and self.zoneUpdate then + local inupdatezone=self.tanker:GetUnit(1):IsInZone(self.zoneUpdate) + if inupdatezone then + local clock=UTILS.SecondsToClock(timer.getAbsTime()) + self:T(string.format("Recovery tanker is in pattern update zone! Time=%s", clock)) + end end -- Check if tanker is running and not RTBing or refueling. @@ -798,7 +812,7 @@ end function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) -- Debug message. - self:T(string.format("Updating recovery tanker %s orbit.", self.tanker:GetName())) + self:T(string.format("Updating recovery tanker %s racetrack pattern.", self.tanker:GetName())) -- Carrier heading. local hdg=self.carrier:GetHeading() @@ -1102,18 +1116,12 @@ end -- @return #boolean If true, heading and/or position have changed more than 10 degrees or 10 km, respectively. function RECOVERYTANKER:_CheckPatternUpdate(dt) - -- Assume no update necessary. - local update=false - - local Hchange=false - local Dchange=false - local turning=false - -- Get current position and orientation of carrier. local pos=self.carrier:GetCoordinate() + + -- Current orientation of carrier. local vNew=self.carrier:GetOrientationX() - -- Reference orientation of carrier after the last update local vOld=self.orientation @@ -1121,7 +1129,7 @@ function RECOVERYTANKER:_CheckPatternUpdate(dt) local vLast=self.orientlast -- We only need the X-Z plane. - vNew.y=0 ; vOld.y=0 + vNew.y=0 ; vOld.y=0 ; vLast.y=0 -- Get angle between old and new orientation vectors in rad and convert to degrees. local deltaHeading=math.deg(math.acos(UTILS.VecDot(vNew,vOld)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vOld))) @@ -1132,11 +1140,17 @@ function RECOVERYTANKER:_CheckPatternUpdate(dt) -- Last orientation becomes new orientation self.orientlast=vNew - -- Carrier is turning + -- Carrier is turning when its heading changed by at least one degree since last check. local turning=deltaLast>=1 + + -- Debug output if turning + if turning then + self:T2(string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) + end -- Check if orientation changed. - if math.abs(deltaHeading)>self.Hupdate then + local Hchange=false + if math.abs(deltaHeading)>=self.Hupdate then self:T(string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) Hchange=true end @@ -1145,15 +1159,16 @@ function RECOVERYTANKER:_CheckPatternUpdate(dt) local dist=pos:Get2DDistance(self.position) -- Check if carrier moved more than ~10 km. + local Dchange=false if dist>self.Dupdate then - self:T(string.format("Carrier position changed by %.1f km. Turning=%s.", dist/1000, tostring(turning))) + self:T(string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) Dchange=true end -- Assume no update necessary. local update=false - -- No update if currently turning! Also must be running (nor RTB or refuelling) and T>~10 min. + -- No update if currently turning! Also must be running (not RTB or refuelling) and T>~10 min since last position update. if self:IsRunning() and dt>self.dTupdate and not turning then -- Update if heading or distance changed. @@ -1249,5 +1264,4 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 9eee42af4..6539918ac 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -48,12 +48,12 @@ BIGSMOKEPRESET = { -- @field #string Caucasus Caucasus map. -- @field #string Normandy Normandy map. -- @field #string NTTR Nevada Test and Training Range map. --- @field #string PersionGulf Persian Gulf map. +-- @field #string PersianGulf Persian Gulf map. DCSMAP = { Caucasus="Caucasus", - NTTR="NTTR", + NTTR="Nevada", Normandy="Normandy", - PersianGulf="Persian Gulf" + PersianGulf="PersianGulf" } --- Utilities static class. @@ -758,21 +758,27 @@ end --- Returns the magnetic declination of the map. -- Returned values for the current maps are: -- --- * Caucasus +6 --- * NTTR ? --- * Normandy ? --- * Persion Gulf ? +-- * Caucasus +6 (East), year ~ 2011 +-- * NTTR +12 (East), year ~ 2011 +-- * Normandy -10 (West), year ~ 1944 +-- * Persian Gulf +2 (East), year ~ 2011 -- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre --- @return #string Declination in degrees. +-- @return #number Declination in degrees. function UTILS.GetMagneticDeclination(map) -- Map. map=map or UTILS.GetDCSMap() local declination=0 - if map=="Caucasus" then + if map==DCSMAP.Caucasus then declination=6 - else + elseif map==DCSMAP.NTTR then + declination=12 + elseif map==DCSMAP.Normandy then + declination=-10 + elseif map==DCSMAP.PersianGulf then + declination=2 + else declination=0 end From cfdce853a351007e0595d105bdfaa71bee6f6ac8 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 7 Dec 2018 15:36:41 +0100 Subject: [PATCH 083/147] AIRBOSS w --- Moose Development/Moose/Ops/Airboss.lua | 6 +++--- Moose Development/Moose/Ops/RecoveryTanker.lua | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 2835b5165..d83375c13 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -582,9 +582,9 @@ AIRBOSS.MarshalCall={ --- Difficulty level. -- @type AIRBOSS.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. +-- @field #string EASY Flight Stutdent. Shows tips and hints in important phases of the approach. +-- @field #string NORMAL Naval aviator. Moderate number of hints but not really zip lip. +-- @field #string HARD TOPGUN graduate. For people who know what they are doing. Nearly ziplip. AIRBOSS.Difficulty={ EASY="Flight Student", NORMAL="Naval Aviator", diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 6302c5537..f6c5d596c 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -7,8 +7,9 @@ -- * Regular pattern update with respect to carrier positon. -- * Automatic respawning when tanker runs out of fuel for 24/7 operations. -- * Tanker can be spawned cold or hot on the carrier or at any other airbase or directly in air. +-- * Automatic AA TACAN beacon setting. +-- * Finite State Machine (FSM) implementation, which allows the mission designer to hook into certain events. -- --- Please not that his class is work in progress and in an **alpha** stage. -- -- === -- From db6c7f1a2ce0b29eeb55324d7a0e14476f81dead Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 9 Dec 2018 01:06:01 +0100 Subject: [PATCH 084/147] AIROSS v0.5.0 --- Moose Development/Moose/Core/Radio.lua | 13 +- .../Moose/Functional/Detection.lua | 2 +- Moose Development/Moose/Ops/Airboss.lua | 773 ++++++++++++------ .../Moose/Ops/RecoveryTanker.lua | 2 +- 4 files changed, 509 insertions(+), 281 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 5656ffc40..662b7b94f 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -264,13 +264,12 @@ function RADIO:SetSubtitle(Subtitle, SubtitleDuration) self:E({"Subtitle is invalid. Subtitle reset.", self.Subtitle}) end if type(SubtitleDuration) == "number" then - if math.floor(math.abs(SubtitleDuration)) == SubtitleDuration then - self.SubtitleDuration = SubtitleDuration - return self - end + self.SubtitleDuration = SubtitleDuration + else + self.SubtitleDuration = 0 + self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) end - self.SubtitleDuration = 0 - self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) + return self end --- Create a new transmission, that is to say, populate the RADIO with relevant data @@ -309,7 +308,7 @@ end -- @param #boolean Loop If true, loop message. -- @return #RADIO self function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop) - self:E({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) + self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) -- Set file name. self:SetFileName(FileName) diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 7402729ab..fd3029b5a 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1012,7 +1012,7 @@ do -- DETECTION_BASE --- Set the parameters to calculate to optimal intercept point. -- @param #DETECTION_BASE self -- @param #boolean Intercept Intercept is true if an intercept point is calculated. Intercept is false if it is disabled. The default Intercept is false. - -- @param #number IntereptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. + -- @param #number InterceptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. -- @return #DETECTION_BASE self function DETECTION_BASE:SetIntercept( Intercept, InterceptDelay ) self:F2() diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index d83375c13..34e699e89 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -2,7 +2,7 @@ -- -- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers. -- --- Features: +-- Main features: -- -- * CASE I, II and III recoveries. -- * Supports human pilots as well as AI flight groups. @@ -11,13 +11,16 @@ -- * Define recovery time windows with individual recovery cases. -- * Automatic TACAN and ICLS channel setting of carrier. -- * Separate radio channels for LSO and Marshal transmissions. --- * Voice over support for LSO and Marshal and Airboss radio transmissions. +-- * Voice over support for LSO and Marshal radio transmissions. -- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, help function (player aircraft attitude, marking of pattern zones etc). -- * Recovery tanker and refueling option via integration of @{#Ops.RecoveryTanker} class. --- * Rescue helo option via @{#Ops.RescueHelo} class. --- * Multiple carrier support (due to object oriented approach). +-- * Rescue helo option via @{#Ops.RescueHelo} class. +-- * Highly customizable by user API functions. +-- * Multiple carrier support due to object oriented approach. +-- * Finite State Machine (FSM) implementation. -- -- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much work in progress. +-- Your constructive feed back is necessary and highly appreciated. -- -- At the moment, parameters are optimized for F/A-18C Hornet as aircraft and USS John C. Stennis as carrier. -- The community A-4E mod is also supported in priciple but maybe needs further tweaking of parameters such as on speed AoA values. @@ -27,7 +30,7 @@ -- === -- -- ### Author: **funkyfranky** --- ### Special Thanks To: **Bankler** (Carrier trainer idea and script) +-- ### Special thanks to **Bankler** for his [https://forums.eagle.ru/showthread.php?t=221412](Recovery Trainer) mission and script, which gave the inspiration for this class. -- -- @module Ops.Airboss -- @image MOOSE.JPG @@ -35,8 +38,8 @@ --- AIRBOSS class. -- @type AIRBOSS -- @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 #string lid Class id string for output to DCS log file. -- @field Wrapper.Unit#UNIT carrier Aircraft carrier unit on which we want to practice. -- @field #string carriertype Type name of aircraft carrier. -- @field #AIRBOSS.CarrierParameters carrierparam Carrier specifc parameters. @@ -50,12 +53,12 @@ -- @field #boolean ICLSon Automatic ICLS is activated. -- @field #number ICLSchannel ICLS channel. -- @field #string ICLSmorse ICLS morse code, e.g. "STN". --- @field Core.Radio#RADIO LSOradio Radio for LSO calls. --- @field #number LSOfreq LSO radio frequency in MHz. --- @field #string LSOmodulation LSO radio modulation "AM" or "FM". --- @field Core.Radio#RADIO Carrierradio Radio for carrier calls. --- @field #number Carrierfreq Marshal radio frequency in MHz. --- @field #string Carriermodulation Marshal radio modulation "AM" or "FM". +-- @field Core.Radio#RADIO LSORadio Radio for LSO calls. +-- @field #number LSOFreq LSO radio frequency in MHz. +-- @field #string LSOModu LSO radio modulation "AM" or "FM". +-- @field Core.Radio#RADIO MarshalRadio Radio for carrier calls. +-- @field #number MarshalFreq Marshal radio frequency in MHz. +-- @field #string MarshalModu Marshal radio modulation "AM" or "FM". -- @field Core.Scheduler#SCHEDULER radiotimer Radio queue scheduler. -- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. @@ -84,6 +87,7 @@ -- @field #table RQMarshal Radio queue of marshal. -- @field #table RQLSO Radio queue of LSO. -- @field #number Nmaxpattern Max number of aircraft in landing pattern. +-- @field #boolean handleai If true (default), handle AI aircraft. -- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. -- @field Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. -- @extends Core.Fsm#FSM @@ -96,7 +100,7 @@ -- -- # The AIRBOSS Concept -- --- On an aircraft carrier, the AIRBOSS is guy who is in charge! +-- On a carrier, the AIRBOSS is guy who is really in charge - don't mess with him! -- -- # Recovery Cases -- @@ -105,6 +109,10 @@ -- * CASE I: For daytime and good weather, -- * CASE II: For daytime but poor visibility conditions, -- * CASE III: For nighttime recoveries. +-- +-- That being said, this script allows you to use any of the three cases to be used at any time. Or, in other words, *you* need to specify when which case is safe and appropriate. +-- +-- This is a lot of responsability. *You* are the boss, but *you* need to make the right decisions or things will go terribly wrong! -- -- ## CASE I -- @@ -142,12 +150,12 @@ AIRBOSS = { ICLSon = nil, ICLSchannel = nil, ICLSmorse = nil, - LSOradio = nil, - LSOfreq = nil, - LSOmodulation = nil, - Carrierradio = nil, - Carrierfreq = nil, - Carriermodulation = nil, + LSORadio = nil, + LSOFreq = nil, + LSOModu = nil, + MarshalRadio = nil, + MarshalFreq = nil, + MarshalModu = nil, radiotimer = nil, zoneCCA = nil, zoneCCZ = nil, @@ -176,8 +184,12 @@ AIRBOSS = { RQMarshal = {}, RQLSO = {}, Nmaxpattern = nil, + handleai = nil, tanker = nil, warehouse = nil, + Corientation = nil, + Corientlast = nil, + Cposition = nil, } --- Player aircraft types capable of landing on carriers. @@ -234,6 +246,7 @@ AIRBOSS.CarrierType={ -- @field #number wire2 Distance in meters from carrier position to second wire. -- @field #number wire3 Distance in meters from carrier position to third wire. -- @field #number wire4 Distance in meters from carrier position to fourth wire. +-- @field #number wireoffset Offset in meters for wire calculation. --- Aircraft specific Angle of Attack (AoA) (or alpha) parameters. -- @type AIRBOSS.AircraftAoA @@ -512,70 +525,70 @@ AIRBOSS.MarshalCall={ file="LSO-N0", suffix="ogg", loud=false, - subtitle="0", + subtitle="", duration=0.40, }, N1={ file="LSO-N1", suffix="ogg", loud=false, - subtitle="1", + subtitle="", duration=0.25, }, N2={ file="LSO-N2", suffix="ogg", loud=false, - subtitle="2", + subtitle="", duration=0.37, }, N3={ file="LSO-N3", suffix="ogg", loud=false, - subtitle="3", + subtitle="", duration=0.37, }, N4={ file="LSO-N4", suffix="ogg", loud=false, - subtitle="4", + subtitle="", duration=0.39, }, N5={ file="LSO-N5", suffix="ogg", loud=false, - subtitle="5", + subtitle="", duration=0.38, }, N6={ file="LSO-N6", suffix="ogg", loud=false, - subtitle="6", + subtitle="", duration=0.40, }, N7={ file="LSO-N7", suffix="ogg", loud=false, - subtitle="7", + subtitle="", duration=0.40, }, N8={ file="LSO-N8", suffix="ogg", loud=false, - subtitle="8", + subtitle="", duration=0.37, }, N9={ file="LSO-N9", suffix="ogg", loud=false, - subtitle="9", + subtitle="", duration=0.40, --0.38 too short }, } @@ -593,10 +606,12 @@ AIRBOSS.Difficulty={ --- Recovery window parameters. -- @type AIRBOSS.Recovery --- @field #number START Start of recovery in seconds of abs time. --- @field #number STOP End of recovery in seconds of abs time. +-- @field #number START Start of recovery in seconds of abs mission time. +-- @field #number STOP End of recovery in seconds of abs mission time. -- @field #number CASE Recovery case (1-3) of that time slot. -- @field #number OFFSET Angle offset of the holding pattern in degrees. Usually 0, +-15, or +-30 degrees. +-- @field #boolean OPEN Recovery window is currently open. +-- @field #boolean OVER Recovery window is over and closed. --- Groove position. -- @type AIRBOSS.GroovePos @@ -626,12 +641,13 @@ AIRBOSS.GroovePos={ -- @field #number LUE Lineup error in degrees. -- @field #number Roll Roll angle. -- @field #number Rhdg Relative heading player to carrier. 0=parallel, +-90=perpendicular. +-- @field #number TGroove Time stamp when pilot entered the groove. --- LSO grade -- @type AIRBOSS.LSOgrade -- @field #string grade LSO grade, i.e. _OK_, OK, (OK), --, CUT -- @field #number points Points received. --- @field #string details Detailed flight analyis analysis. +-- @field #string details Detailed flight analysis. --- Checkpoint parameters triggering the next step in the pattern. -- @type AIRBOSS.Checkpoint @@ -665,6 +681,7 @@ AIRBOSS.GroovePos={ -- @field #number case Recovery case of flight. -- @field #string seclead Name of section lead. -- @field #table section Other human flight groups belonging to this flight. This flight is the lead. +-- @field #boolean ballcall If true, flight called the ball in the groove. --- Player data table holding all important parameters of each player. -- @type AIRBOSS.PlayerData @@ -686,6 +703,8 @@ AIRBOSS.GroovePos={ -- @field #boolean patternwo If true, player was waved of during the pattern. -- @field #boolean lig If true, player was long in the groove. -- @field #number Tlso Last time the LSO gave an advice. +-- @field #number Tgroove Time in the groove in seconds. +-- @field #number wire Wire caught by player when trapped. -- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. -- @field #table menu F10 radio menu -- @extends #AIRBOSS.Flightitem @@ -696,23 +715,23 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.4.9" +AIRBOSS.version="0.5.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Update AI holding pattern wrt to moving carrier. -- TODO: Extract (static) weather from mission for cloud covery etc. -- TODO: Option to filter AI groups for recovery. --- TODO: Option to turn AI handling off. -- TODO: Check distance to players during approach. PWO if too close. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! --- TODO: Add user functions. --- TODO: Generalize parameters for other carriers. --- TODO: Generalize parameters for other aircraft. -- TODO: Foul deck check. -- TODO: Persistence of results. +-- DONE: Option to turn AI handling off. +-- DONE: Add user functions. +-- DONE: Update AI holding pattern wrt to moving carrier. +-- DONE: Generalize parameters for other carriers. +-- DONE: Generalize parameters for other aircraft. -- DONE: Add radio check (LSO, AIRBOSS) to F10 radio menu. -- DONE: Right pattern step after bolter/wo/patternWO? Guess so. -- DONE: Set case II and III times (via recovery time). @@ -780,14 +799,14 @@ function AIRBOSS:New(carriername, alias) -- Defaults: -- Set up Airboss radio. - self.Carrierradio=RADIO:New(self.carrier) - self.Carrierradio:SetAlias("MARSHAL") - self:SetCarrierradio() + self.MarshalRadio=RADIO:New(self.carrier) + self.MarshalRadio:SetAlias("MARSHAL") + self:SetMarshalRadio() -- Set up LSO radio. - self.LSOradio=RADIO:New(self.carrier) - self.LSOradio:SetAlias("LSO") - self:SetLSOradio() + self.LSORadio=RADIO:New(self.carrier) + self.LSORadio:SetAlias("LSO") + self:SetLSORadio() -- Radio scheduler. self.radiotimer=SCHEDULER:New() @@ -799,13 +818,16 @@ function AIRBOSS:New(carriername, alias) self:SetTACAN() -- Set max aircraft in landing pattern. - self:SetMaxLandingPattern(2) + self:SetMaxLandingPattern() + + -- Set AI handling On. + self:SetHandleAION() -- Default recovery case. This sets self.defaultcase and self.case. self:SetRecoveryCase(1) -- Set holding offset to 0 degrees. This set self.defaultoffset and self.holdingoffset. - self:SetHoldingOffsetAngle(15) + self:SetHoldingOffsetAngle() -- CCA 50 NM radius zone around the carrier. self:SetCarrierControlledArea() @@ -866,11 +888,11 @@ function AIRBOSS:New(carriername, alias) text=text..string.format("\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring(call.loud), call.subtitle) -- Radio transmission to queue. - self:RadioTransmission(self.LSOradio, call, false, 10) + self:RadioTransmission(self.LSORadio, call, false, 10) -- Also play the loud version. if call.loud then - self:RadioTransmission(self.LSOradio, call, true, 10) + self:RadioTransmission(self.LSORadio, call, true, 10) end end self:I(self.lid..text) @@ -1003,8 +1025,10 @@ end -- @return #AIRBOSS self function AIRBOSS:SetRecoveryCase(case) + -- Set default case or 1. self.defaultcase=case or 1 + -- Current case init. self.case=self.defaultcase return self @@ -1017,9 +1041,10 @@ end -- @return #AIRBOSS self function AIRBOSS:SetHoldingOffsetAngle(offset) - + -- Set default angle or 0. self.defaultoffset=offset or 0 + -- Current offset init. self.holdingoffset=self.defaultoffset return self @@ -1034,8 +1059,14 @@ end -- @return #AIRBOSS self function AIRBOSS:AddRecoveryTime(starttime, stoptime, case, holdingoffset) + -- Absolute mission time in seconds. + local Tnow=timer.getAbsTime() + + -- Input or now. + starttime=starttime or UTILS.SecondsToClock(Tnow) + -- Set start time. - local Tstart=UTILS.ClockToSeconds(starttime or UTILS.SecondsToClock(timer.getAbsTime())) + local Tstart=UTILS.ClockToSeconds(starttime) -- Set stop time. local Tstop=UTILS.ClockToSeconds(stoptime or Tstart+90*60) @@ -1045,6 +1076,10 @@ function AIRBOSS:AddRecoveryTime(starttime, stoptime, case, holdingoffset) self:E(string.format("ERROR: Recovery stop time %s lies before recovery start time %s! Recovery windows rejected.", UTILS.SecondsToClock(Tstart), UTILS.SecondsToClock(Tstop))) return self end + if Tstop<=Tnow then + self:E(string.format("ERROR: Recovery stop time %s already over. Tnow=%s! Recovery windows rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow))) + return self + end -- Case or default value. case=case or self.defaultcase @@ -1052,6 +1087,10 @@ function AIRBOSS:AddRecoveryTime(starttime, stoptime, case, holdingoffset) -- Holding offset or default value. holdingoffset=holdingoffset or self.defaultoffset + -- Offset zero for case I. + if case==1 then + holdingoffset=0 + end -- Recovery window. local recovery={} --#AIRBOSS.Recovery @@ -1059,6 +1098,8 @@ function AIRBOSS:AddRecoveryTime(starttime, stoptime, case, holdingoffset) recovery.STOP=Tstop recovery.CASE=case recovery.OFFSET=holdingoffset + recovery.OPEN=false + recovery.OVER=false -- Add to table table.insert(self.recoverytimes, recovery) @@ -1116,19 +1157,19 @@ end -- @param #number frequency Frequency in MHz. Default 264 MHz. -- @param #string modulation Modulation, i.e. "AM" (default) or "FM". -- @return #AIRBOSS self -function AIRBOSS:SetLSOradio(frequency, modulation) +function AIRBOSS:SetLSORadio(frequency, modulation) - self.LSOfreq=frequency or 264 - self.LSOmodulation=modulation or "AM" + self.LSOFreq=frequency or 264 + self.LSOModu=modulation or "AM" if modulation=="FM" then - self.LSOmodulation=radio.modulation.FM + self.LSOModu=radio.modulation.FM else - self.LSOmodulation=radio.modulation.AM + self.LSOModu=radio.modulation.AM end - self.LSOradio:SetFrequency(self.LSOfreq) - self.LSOradio:SetModulation(self.LSOmodulation) + self.LSORadio:SetFrequency(self.LSOFreq) + self.LSORadio:SetModulation(self.LSOModu) return self end @@ -1138,19 +1179,19 @@ end -- @param #number frequency Frequency in MHz. Default 305 MHz. -- @param #string modulation Modulation, i.e. "AM" (default) or "FM". -- @return #AIRBOSS self -function AIRBOSS:SetCarrierradio(frequency, modulation) +function AIRBOSS:SetMarshalRadio(frequency, modulation) - self.Carrierfreq=frequency or 305 - self.Carrriermodulation=modulation or "AM" + self.MarshalFreq=frequency or 305 + self.MarshalModu=modulation or "AM" if modulation=="FM" then - self.Carriermodulation=radio.modulation.FM + self.MarshalModu=radio.modulation.FM else - self.Carriermodulation=radio.modulation.AM + self.MarshalModu=radio.modulation.AM end - self.Carrierradio:SetFrequency(self.Carrierfreq) - self.Carrierradio:SetModulation(self.Carriermodulation) + self.MarshalRadio:SetFrequency(self.MarshalFreq) + self.MarshalRadio:SetModulation(self.MarshalModu) return self end @@ -1164,6 +1205,23 @@ function AIRBOSS:SetMaxLandingPattern(nmax) return self end +--- Handle AI aircraft. +-- @param #AIRBOSS self +-- @return #ARIBOSS self +function AIRBOSS:SetHandleAION() + self.handleai=true + return self +end + +--- Do not handle AI aircraft. +-- @param #AIRBOSS self +-- @return #ARIBOSS self +function AIRBOSS:SetHandleAIOFF() + self.handleai=false + return self +end + + --- Define recovery tanker associated with the carrier. -- @param #AIRBOSS self -- @param Ops.RecoveryTanker#RECOVERYTANKER recoverytanker Recovery tanker object. @@ -1237,12 +1295,19 @@ function AIRBOSS:onafterStart(From, Event, To) -- TODO: id's to self to be able to stop the scheduler. local RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQLSO, "LSO"}, 1, 0.01) local RQMid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQMarshal, "MARSHAL"}, 1, 0.01) + + + -- Initial carrier position and orientation. + self.Cposition=self:GetCoordinate() + self.Corientation=self.carrier:GetOrientationX() + self.Corientlast=self.Corientation + self.Tpupdate=timer.getTime() -- Start status check in 1 second. self:__Status(1) end ---- On after Status event. Checks player status. +--- On after Status event. Checks for new flights, updates queue and checks player status. -- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. @@ -1254,9 +1319,12 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Update marshal and pattern queue every 30 seconds. if time-self.Tqueue>30 then + + -- Get time. + local clock=UTILS.SecondsToClock(timer.getAbsTime()) -- Debug info. - local text=string.format("Status %s.", self:GetState()) + local text=string.format("Time %s - Status %s (case %d)", clock, self:GetState(), self.case) self:I(self.lid..text) -- Check recovery times and start/stop recovery mode if necessary. @@ -1268,17 +1336,73 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Check marshal and pattern queues. self:_CheckQueue() + -- Check if marshal pattern of AI needs an update. + self:_CheckPatternUpdate() + -- Time stamp. self.Tqueue=time end -- Check player status. self:_CheckPlayerStatus() + + -- Check AI landing pattern status + self:_CheckAIStatus() -- Call status every 0.5 seconds. self:__Status(-0.5) end +--- Check recovery times and start/stop recovery mode of aircraft. +-- @param #AIRBOSS self +function AIRBOSS:_CheckAIStatus() + + -- Loop over all flights in landing pattern. + for _,_flight in pairs(self.Qpattern) do + local flight=_flight --#AIRBOSS.Flightitem + + -- Only AI! + if flight.ai then + + -- Get unnits + local units=flight.group:GetUnits() + + -- Loop over all units in AI flight. + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Get lineup and distance to carrier. + local lineup=self:_Lineup(unit, true) + local distance=unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) + + -- Check if parameters are right and flight is in the groove. + if lineup<2 and distance<=UTILS.NMToMeters(0.75) and not flight.ballcall then + + -- Paddles: Call the ball! + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.CALLTHEBALL, false, 0) + + -- Pilot: "405, Hornet Ball, 3.2" + -- TODO: Hornet ==> General. + -- TODO: Message to players only. + -- TODO: Voice over. + -- TODO: Correct unit onboard number not section lead! + local text=string.format("%s, Hornet Ball, %.1f.", flight.onboard, self:_GetFuelState(unit)/1000) + MESSAGE:New(text, 5):ToCoalition(self:GetCoalition()) + --self:MessageToPlayer(playerData, text, playerData.onboard, "", 3, false, 3) + + -- Paddles: Roger ball. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 10) + + -- TODO: This does not work for flights with more than one aircraft in the group! + flight.ballcall=true + end + + end + end + end + +end + --- Check recovery times and start/stop recovery mode of aircraft. -- @param #AIRBOSS self function AIRBOSS:_CheckRecoveryTimes() @@ -1305,6 +1429,8 @@ function AIRBOSS:_CheckRecoveryTimes() -- Loop over all slots. for _,_recovery in pairs(self.recoverytimes) do local recovery=_recovery --#AIRBOSS.Recovery + + --if recovery.OVER==false then -- Get start/stop clock strings. local Cstart=UTILS.SecondsToClock(recovery.START) @@ -1325,21 +1451,28 @@ function AIRBOSS:_CheckRecoveryTimes() state="in progress" else -- Start recovery. - self:RecoveryStart(recovery.CASE) + self:RecoveryStart(recovery.CASE, recovery.OFFSET) state="starting now" + recovery.OPEN=true end else -- Stop time has passed. - if self:IsRecovering() then + if self:IsRecovering() and not recovery.OVER then -- Set carrier to idle. self:RecoveryStop() - state="stopping now" + state="closing now" + + -- Closed. + recovery.OPEN=false + + -- Window just closed. + recovery.OVER=true else -- Carrier is already idle. - state="over and stopped" + state="closed" end end @@ -1351,12 +1484,12 @@ function AIRBOSS:_CheckRecoveryTimes() -- This is the next to come. if nextwindow==nil then nextwindow=recovery - state="next to come" + state="next in line" end end -- Debug text. - text=text..string.format("\n- Start=%s Stop=%s Case=%d Offset=%d Status=\"%s\"", Cstart, Cstop, recovery.CASE, recovery.OFFSET, state) + text=text..string.format("\n- Start=%s Stop=%s Case=%d Offset=%d Open=%s Closed=%s Status=\"%s\"", Cstart, Cstop, recovery.CASE, recovery.OFFSET, tostring(recovery.OPEN), tostring(recovery.OVER), state) end -- Debug output. @@ -1488,6 +1621,7 @@ function AIRBOSS:_InitStennis() self.carrierparam.wire2 = -92 self.carrierparam.wire3 = -80 self.carrierparam.wire4 = -68 + self.carrierparam.wireoffset = 30 -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight. self.Platform.name="Platform 5k" @@ -1913,8 +2047,12 @@ function AIRBOSS:_ScanCarrierZone() -- Create a new flight group if knownflight then + + -- Debug output. self:T2(self.lid..string.format("Known flight group %s of type %s in CCA.", groupname, actype)) - if knownflight.ai then + + -- Check if flight is AI and if we want to handle it at all. + if knownflight.ai and self.handleai then -- Get distance to carrier. local dist=knownflight.group:GetCoordinate():Get2DDistance(self:GetCoordinate()) @@ -1923,7 +2061,7 @@ function AIRBOSS:_ScanCarrierZone() local closein=knownflight.dist0-dist -- Debug info. - self:T3(self.lid..string.format("Known flight group %s closed in by %.1f NM", knownflight.groupname, UTILS.MetersToNM(closein))) + self:T3(self.lid..string.format("Known AI flight group %s closed in by %.1f NM", knownflight.groupname, UTILS.MetersToNM(closein))) -- Send AI flight to marshal stack if group closes in more than 2.5 and has initial flag value. if closein>UTILS.NMToMeters(2.5) and knownflight.flag:Get()==-100 then @@ -1944,9 +2082,9 @@ function AIRBOSS:_ScanCarrierZone() -- Add group to marshal stack queue. self:_AddMarshalGroup(knownflight, stack) - end - end - end + end -- Tanker + end -- Closed in + end -- AI else -- Unknown new flight. Create a new flight group. self:_CreateFlightGroup(group) @@ -2052,19 +2190,40 @@ function AIRBOSS:_MarshalAI(flight, nstack) -- TODO: skip stack 6 if recoverytanker (or at whatever angels the tanker orbits). -- Get altitude and positions. - local Altitude, p1, p2=self:_GetMarshalAltitude(stack) + local Altitude, p1, p2=self:_GetMarshalAltitude(stack, flight.case) - local p1=p1 --Core.Point#COORDINATE + -- Right CW pattern for CASE II/III. + local c1=nil --Core.Point#COORDINATE + local c2=nil --Core.Point#COORDINATE + local p0=nil --Core.Point#COORDINATE + if flight.case==1 then + c1=p1 + p0=self:GetCoordinate() + else + c1=p2 + c2=p1 + p0=c2 + end + + -- Distance to the boat. local Dist=p1:Get2DDistance(self:GetCoordinate()) -- Task: orbit at specified position, altitude and speed until flag=stack-1 - local TaskOrbit=_taskorbit(p1, Altitude, Speed, stack-1, p2) + local TaskOrbit=_taskorbit(c1, Altitude, Speed, stack-1, c2) -- Waypoint description. - local text=string.format("Marshal @ alt=%d ft, dist=%.1f NM, speed=%d knots", UTILS.MetersToFeet(Altitude), UTILS.MetersToNM(Dist), UTILS.MpsToKnots(Speed)) + local text=string.format("Flight %s: Marshal stack %d: alt=%d, dist=%.1f, speed=%d", flight.groupname, stack, UTILS.MetersToFeet(Altitude), UTILS.MetersToNM(Dist), UTILS.MpsToKnots(Speed)) + + -- Debug mark. + if self.Debug then + c1:MarkToAll(text) + if c2 then + c2:MarkToAll(text) + end + end -- Waypoint. - wp[#wp+1]=p1:SetAltitude(Altitude):WaypointAirTurningPoint(nil, Speed, {TaskOrbit}, text) + wp[#wp+1]=p0:SetAltitude(Altitude):WaypointAirTurningPoint(nil, Speed, {TaskOrbit}, text) end @@ -2121,7 +2280,7 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) angels0=6 -- Distance: d=n*angles0+15 NM, so first stack is at 15+6=21 NM - Dist=UTILS.NMToMeters(stack*angels0+15) + Dist=UTILS.NMToMeters((stack-1)+angels0+15) -- Get correct radial depending on recovery case including offset. local radial @@ -2145,8 +2304,6 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) return altitude, p1, p2 end - - --- Add a flight group to a specific marshal stack and to the marshal queue. -- @param #AIRBOSS self -- @param #AIRBOSS.Flightitem flight Flight group. @@ -2314,13 +2471,16 @@ function AIRBOSS:_GetFreeStack(case) nfull=self:_GetQueueInfo(self.Qmarshal, 23) end + -- Simple case without a recovery tanker for now. + local nfree=nfull+1 + + --[[ -- Get recovery tanker stack. local tankerstack=9999 if self.tanker and case==1 then tankerstack=self:_GetAngels(self.tanker.altitude) end - local nfree if nfull=1 + + -- No update if carrier is turning! + if turning then + self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) + return + end + + -- Check if orientation changed. + local Hchange=false + if math.abs(deltaHeading)>=Hupdate then + self:T(self.lid..string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) + Hchange=true + end + + -- Get distance to saved position. + local dist=pos:Get2DDistance(self.Cposition) + + -- Check if carrier moved more than ~10 km. + local Dchange=false + if dist>=Dupdate then + self:T(self.lid..string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) + Dchange=true + end + + -- If heading or distance changed ==> update marshal AI patterns. + if Hchange or Dchange then + + -- Loop over all marshal flights + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.Flightitem + + -- Update marshal pattern of AI keeping the same stack. + if flight.ai then + self:_MarshalAI(flight, flight.flag:Get()) + end + + end + + -- Reset parameters for next update check. + self.Corientation=vNew + self.Cposition=pos + self.Tpupdate=timer.getTime() + end + +end + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Player Status ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2903,7 +3163,7 @@ function AIRBOSS:OnEventBirth(EventData) -- Debug output. local text=string.format("AIRBOSS: Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName()) self:T(self.lid..text) - MESSAGE:New(text, 5):ToAllIf(self.Debug) + MESSAGE:New(text, 5):ToAllIf(self.Debug or true) -- Check if aircraft type the player occupies is carrier capable. local rightaircraft=self:_IsCarrierAircraft(_unit) @@ -2922,8 +3182,8 @@ function AIRBOSS:OnEventBirth(EventData) -- Debug. if self.Debug then - self:_Number2Sound(self.LSOradio, "0123456789", 10) - self:_Number2Sound(self.Carrierradio, "0123456789", 20) + self:_Number2Sound(self.LSORadio, "0123456789", 10) + self:_Number2Sound(self.MarshalRadio, "0123456789", 20) end end @@ -2964,7 +3224,7 @@ function AIRBOSS:OnEventLand(EventData) -- Debug output. local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename) self:I(self.lid..text) - MESSAGE:New(text, 5):ToAllIf(self.Debug) + MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.Debug) -- Player data. local playerData=self.players[_playername] --#AIRBOSS.PlayerData @@ -2999,13 +3259,20 @@ function AIRBOSS:OnEventLand(EventData) end -- Get wire - local wire=self:_GetWire(dist, 30) + local wire=self:_GetWire(dist) + + -- Get time in the groove. + local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData + playerData.Tgroove=timer.getTime()-gdataX0.TGroove + + -- Set player wire + playerData.wire=wire -- Aircraft type. local _type=EventData.IniUnit:GetTypeName() -- Debug text. - local text=string.format("Player %s AC type %s landed at dist=%.1f m. Trapped wire=%d.", EventData.IniUnitName, _type, dist, wire) + local text=string.format("Player %s AC type %s landed at dist=%.1f m (+offset=%.1f). Trapped wire=%d.", EventData.IniUnitName, _type, dist, self.carrierparam.wireoffset, wire) text=text..string.format("X=%.1f m, Z=%.1f m, rho=%.1f m, phi=%.1f deg.", X, Z, rho, phi) self:I(self.lid..text) @@ -3016,7 +3283,7 @@ function AIRBOSS:OnEventLand(EventData) playerData.step=AIRBOSS.PatternStep.UNDEFINED -- Call trapped function in 3 seconds to make sure we did not bolter. - SCHEDULER:New(nil, self._Trapped,{self, playerData, wire}, 3) + SCHEDULER:New(nil, self._Trapped,{self, playerData}, 3) end else @@ -3236,11 +3503,11 @@ function AIRBOSS:_Initial(playerData) end ---- Platform at 5k ft for case II/III recoveries. Descent at 2000 ft/min. +--- Check if player is in CASE II/III approach corridor. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Platform(playerData) - +function AIRBOSS:_CheckCorridor(playerData) + -- Check if player is in valid zone local validzone=self:_GetZoneCorridor(playerData.case) @@ -3258,6 +3525,16 @@ function AIRBOSS:_Platform(playerData) self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARSHAL") playerData.warning=false end + +end + +--- Platform at 5k ft for case II/III recoveries. Descent at 2000 ft/min. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Platform(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZonePlatform(playerData.case)) @@ -3266,7 +3543,7 @@ function AIRBOSS:_Platform(playerData) if inzone then -- Debug message. - MESSAGE:New("Platform step reached", 5):ToAllIf(self.Debug) + MESSAGE:New("Platform step reached", 5, "DEBUG"):ToAllIf(self.Debug) -- Get optimal altitiude. local altitude, aoa, distance, speed =self:_GetAircraftParameters(playerData) @@ -3306,23 +3583,8 @@ end -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_ArcInTurn(playerData) - -- Check if player is in valid zone - local validzone=self:_GetZoneCorridor(playerData.case) - - -- Check if we are inside the moving zone. - local invalid=playerData.unit:IsNotInZone(validzone) - - -- Issue warning. - if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "MARSHAL") - playerData.warning=true - end - - -- Back in zone. - if not invalid and playerData.warning then - self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARSHAL") - playerData.warning=false - end + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZoneArcIn(playerData.case)) @@ -3330,7 +3592,7 @@ function AIRBOSS:_ArcInTurn(playerData) if inzone then -- Debug message. - MESSAGE:New("Arc Turn In step reached", 5):ToAllIf(self.Debug) + MESSAGE:New("Arc Turn In step reached", 5, "DEBUG"):ToAllIf(self.Debug) -- Get optimal altitiude. local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) @@ -3355,24 +3617,8 @@ end -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_ArcOutTurn(playerData) - -- Check if player is in valid zone - local validzone=self:_GetZoneCorridor(playerData.case) - - -- Check if we are inside the moving zone. - local invalid=playerData.unit:IsNotInZone(validzone) - - -- Issue warning. - if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "MARHAL") - playerData.warning=true - end - - -- Back in zone. - if not invalid and playerData.warning then - self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARSHAL") - playerData.warning=false - end - + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZoneArcOut(playerData.case)) @@ -3381,7 +3627,7 @@ function AIRBOSS:_ArcOutTurn(playerData) if inzone then -- Debug message. - MESSAGE:New("Arc Turn Out step reached", 5):ToAllIf(self.Debug) + MESSAGE:New("Arc Turn Out step reached", 5, "DEBUG"):ToAllIf(self.Debug) -- Get optimal altitiude. local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) @@ -3414,32 +3660,16 @@ end -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_DirtyUp(playerData) - -- Check if player is in valid zone - local validzone=self:_GetZoneCorridor(playerData.case) - - -- Check if we are inside the moving zone. - local invalid=playerData.unit:IsNotInZone(validzone) - - -- Issue warning. - if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "MARSHAL") - playerData.warning=true - end + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) - -- Back in zone. - if not invalid and playerData.warning then - self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARSHAL") - playerData.warning=false - end - - -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) if inzone then -- Debug message. - MESSAGE:New("Dirty up step reached", 5):ToAllIf(self.Debug) + MESSAGE:New("Dirty up step reached", 5, "DEBUG"):ToAllIf(self.Debug) -- Get optimal altitiude. local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) @@ -3468,24 +3698,8 @@ end -- @param #AIRBOSS.PlayerData playerData Player data table. function AIRBOSS:_Bullseye(playerData) - -- Check if player is in valid zone - local validzone=self:_GetZoneCorridor(playerData.case) - - -- Check if we are inside the moving zone. - local invalid=playerData.unit:IsNotInZone(validzone) - - -- Issue warning. - if invalid and not playerData.warning then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "MARSHAL") - playerData.warning=true - end - - -- Back in zone. - if not invalid and playerData.warning then - self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARHAL") - playerData.warning=false - end - + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) @@ -3495,7 +3709,7 @@ function AIRBOSS:_Bullseye(playerData) if inzone then -- Debug message. - MESSAGE:New("Bullseye step reached", 5):ToAllIf(self.Debug) + MESSAGE:New("Bullseye step reached", 5, "DEBUG"):ToAllIf(self.Debug) -- Get optimal altitiude. local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) @@ -3622,7 +3836,7 @@ function AIRBOSS:_CheckForLongDownwind(playerData) if X5. This would mean the player has not tunred in correctly! + -- TODO: could add angled approach if lineup<5 and relhead>5. This would mean the player has not turned in correctly! - -- Groove + -- Groove data. playerData.groove.X0=groovedata -- Next step: X start & call the ball. @@ -3873,10 +4088,10 @@ function AIRBOSS:_Groove(playerData) end -- Lineup with runway centerline. - local lineupError=self:_Lineup(playerData, true) + local lineupError=self:_Lineup(playerData.unit, true) -- Glide slope. - local glideslopeError=self:_Glideslope(playerData, 3.5) + local glideslopeError=self:_Glideslope(playerData.unit, 3.5) -- Get AoA. local AoA=playerData.unit:GetAoA() @@ -3901,7 +4116,7 @@ function AIRBOSS:_Groove(playerData) if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX then -- LSO "Call the ball" call. - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.CALLTHEBALL) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.CALLTHEBALL) playerData.Tlso=timer.getTime() -- Pilot "405, Hornet Ball, 3.2" @@ -3919,7 +4134,7 @@ function AIRBOSS:_Groove(playerData) elseif rho<=RRB and playerData.step==AIRBOSS.PatternStep.GROOVE_RB then -- LSO "Roger ball" call. - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.ROGERBALL) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL) playerData.Tlso=timer.getTime()+1 -- Store data. @@ -3963,7 +4178,7 @@ function AIRBOSS:_Groove(playerData) if waveoff then -- LSO Wave off! - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.WAVEOFF) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WAVEOFF) playerData.Tlso=timer.getTime() -- Player was waved off! @@ -4031,7 +4246,7 @@ end -- -- * Glide slope error > 3 degrees. -- * Line up error > 3 degrees. --- * AoA<6.9 or AoA>9.3 for TOPGUN graduates. +-- * AoA check but only for TOPGUN graduates. -- @param #AIRBOSS self -- @param #number glideslopeError Glide slope error in degrees. -- @param #number lineupError Line up error in degrees. @@ -4045,13 +4260,13 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) -- Too high or too low? if math.abs(glideslopeError)>1 then - self:I(self.lid..string.format("%s: Wave off due to glide slope error %.1f > 1 degree!", playerData.name, glideslopeError)) + self:I(self.lid..string.format("%s: Wave off due to glide slope error |%.1f| > 1 degree!", playerData.name, glideslopeError)) waveoff=true end -- Too far from centerline? if math.abs(lineupError)>3 then - self:I(self.lid..string.format("%s: Wave off due to line up error %.1f > 3 degrees!", playerData.name, lineupError)) + self:I(self.lid..string.format("%s: Wave off due to line up error |%.1f| > 3 degrees!", playerData.name, lineupError)) waveoff=true end @@ -4079,7 +4294,7 @@ end function AIRBOSS:_GetWire(d, dx) -- Little offset for the exact wire positions. - dx=dx or 30 + dx=dx or self.carrierparam.wireoffset -- Which wire was caught? X>0 since calculated as distance! local wire @@ -4101,14 +4316,15 @@ function AIRBOSS:_GetWire(d, dx) return wire end ---- Trapped? +--- Trapped? Check if in air or not after landing event. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. --- @param #number wire The wire caught. -function AIRBOSS:_Trapped(playerData, wire) +function AIRBOSS:_Trapped(playerData) if playerData.unit:InAir()==false then -- Seems we have successfully landed. + + local wire=playerData.wire -- Message to player. local text=string.format("Trapped %d-wire.", wire) @@ -4125,10 +4341,11 @@ function AIRBOSS:_Trapped(playerData, wire) -- Debrief. local hint = string.format("Trapped %d-wire.", wire) - self:_AddToDebrief(playerData, hint, "Goove: IW") + self:_AddToDebrief(playerData, hint, "Groove: IW") else --Still in air ==> Boltered! + MESSAGE:New("Player boltered in trapped", 5, "DEBUG") playerData.boltered=true end @@ -4248,12 +4465,11 @@ function AIRBOSS:_GetZoneArcOut(case) end - -- Get coordinate and vec2. + -- Get coordinate of carrier and translate. local coord=self:GetCoordinate():Translate(distance, radial) - local vec2=coord:GetVec2() - + -- Create zone. - local zone=ZONE_RADIUS:New("Zone Arc Out", vec2, radius) + local zone=ZONE_RADIUS:New("Zone Arc Out", coord:GetVec2(), radius) return zone end @@ -4293,12 +4509,11 @@ function AIRBOSS:_GetZoneArcIn(case) -- Distance = 14 NM local distance=UTILS.NMToMeters(x) - -- Get coordinate and vec2. + -- Get coordinate. local coord=self:GetCoordinate():Translate(distance, radial) - local vec2=coord:GetVec2() -- Create zone. - local zone=ZONE_RADIUS:New("Zone Arc In", vec2, radius) + local zone=ZONE_RADIUS:New("Zone Arc In", coord:GetVec2(), radius) return zone end @@ -4335,12 +4550,11 @@ function AIRBOSS:_GetZonePlatform(case) -- Distance = 19 NM local distance=UTILS.NMToMeters(19)/math.cos(alpha) - -- Get coordinate and vec2. + -- Get coordinate. local coord=self:GetCoordinate():Translate(distance, radial) - local vec2=coord:GetVec2() -- Create zone. - local zone=ZONE_RADIUS:New("Zone Platform", vec2, radius) + local zone=ZONE_RADIUS:New("Zone Platform", coord:GetVec2(), radius) return zone end @@ -4393,18 +4607,18 @@ function AIRBOSS:_GetZoneCorridor(case) local Q=x2-b -- Debug output. - self:I(string.format("FF case %d radial = %d", case, radial)) - self:I(string.format("FF case %d offset = %d", case, offset)) - self:I(string.format("FF w = %.1f NM", w)) - self:I(string.format("FF l = %.1f NM", l)) - self:I(string.format("FF d = %.1f NM", d)) - self:I(string.format("FF y1 = %.1f NM", y1)) - self:I(string.format("FF x1 = %.1f NM", x1)) - self:I(string.format("FF y2 = %.1f NM", y2)) - self:I(string.format("FF x2 = %.1f NM", x2)) - self:I(string.format("FF b = %.1f NM", b)) - self:I(string.format("FF P = %.1f NM", P)) - self:I(string.format("FF Q = %.1f NM", Q)) + self:T3(string.format("FF case %d radial = %d", case, radial)) + self:T3(string.format("FF case %d offset = %d", case, offset)) + self:T3(string.format("FF w = %.1f NM", w)) + self:T3(string.format("FF l = %.1f NM", l)) + self:T3(string.format("FF d = %.1f NM", d)) + self:T3(string.format("FF y1 = %.1f NM", y1)) + self:T3(string.format("FF x1 = %.1f NM", x1)) + self:T3(string.format("FF y2 = %.1f NM", y2)) + self:T3(string.format("FF x2 = %.1f NM", x2)) + self:T3(string.format("FF b = %.1f NM", b)) + self:T3(string.format("FF P = %.1f NM", P)) + self:T3(string.format("FF Q = %.1f NM", Q)) local c={} c[1]=self:GetCoordinate() --Carrier coordinate @@ -4432,7 +4646,7 @@ function AIRBOSS:_GetZoneCorridor(case) local p={} for _i,_c in ipairs(c) do if self.Debug then - _c:SmokeBlue() + --_c:SmokeBlue() end p[_i]=_c:GetVec2() end @@ -4489,9 +4703,6 @@ function AIRBOSS:_GetZoneHolding(case, stack) p[3]=c2:Translate(UTILS.NMToMeters(7), hdg+90):GetVec2() --p3 6 NM port of carrier. p[4]=c1:Translate(UTILS.NMToMeters(7), hdg+90):GetVec2() --p4 6 NM port of carrier. - --c1:SmokeBlue() - --c2:SmokeOrange() - -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. -- So stay 0-5 NM (+1 NM error margin) port of carrier. zoneHolding=ZONE_POLYGON_BASE:New("CASE II/III Holding Zone", p) @@ -4547,8 +4758,8 @@ function AIRBOSS:_DetailedPlayerStatus(playerData) playerData.step==AIRBOSS.PatternStep.GROOVE_IC or playerData.step==AIRBOSS.PatternStep.GROOVE_AR or playerData.step==AIRBOSS.PatternStep.GROOVE_IW then - local lineup=self:_Lineup(playerData, true) - local glideslope=self:_Glideslope(playerData, 3.5) + local lineup=self:_Lineup(playerData.unit, true) + local glideslope=self:_Glideslope(playerData.unit, 3.5) text=text..string.format("\nLU Error = %.1f° (line up)", lineup) text=text..string.format("\nGS Error = %.1f° (glide slope)", glideslope) end @@ -4559,21 +4770,21 @@ function AIRBOSS:_DetailedPlayerStatus(playerData) MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) end ---- Get glide slope of aircraft. +--- Get glide slope of aircraft unit. -- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. --- @pram #number gangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope. +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @param #number optangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope. -- @return #number Glide slope angle in degrees measured from the deck of the carrier and third wire. -function AIRBOSS:_Glideslope(playerData, gangle) +function AIRBOSS:_Glideslope(unit, optangle) -- Default is 0. - gangle=gangle or 0 + optangle=optangle or 0 -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi = self:_GetDistances(playerData.unit) + local X, Z, rho, phi = self:_GetDistances(unit) -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. - local h=playerData.unit:GetAltitude()-self.carrierparam.deckheight + local h=unit:GetAltitude()-self.carrierparam.deckheight -- Distance correction. local offx=self.carrierparam.wire3 or self.carrierparam.sterndist @@ -4582,19 +4793,19 @@ function AIRBOSS:_Glideslope(playerData, gangle) -- Glide slope. local glideslope=math.atan(h/x) - return math.deg(glideslope)-gangle + return math.deg(glideslope)-optangle end --- Get line up of player wrt to carrier. -- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param Wrapper.Unit#UNIT unit Aircraft unit. -- @param #boolean runway If true, include angled runway. -- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. -- @return #number Distance from carrier tail to player aircraft in meters. -function AIRBOSS:_Lineup(playerData, runway) +function AIRBOSS:_Lineup(unit, runway) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi = self:_GetDistances(playerData.unit) + local X, Z, rho, phi = self:_GetDistances(unit) -- Position at the end of the deck. From there we calculate the angle. local b={x=self.carrierparam.sterndist, z=0} @@ -4763,9 +4974,9 @@ function AIRBOSS:_GetRelativeHeading(unit, runway) return rhdg end ---- Calculate distances between carrier and player unit. +--- Calculate distances between carrier and aircraft unit. -- @param #AIRBOSS self --- @param Wrapper.Unit#UNIT unit Player unit +-- @param Wrapper.Unit#UNIT unit Aircraft 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. @@ -4857,19 +5068,19 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) local text="" if glideslopeError>1 then -- "You're high!" - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.HIGH, true) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.HIGH, true) advice=advice+AIRBOSS.LSOCall.HIGH.duration elseif glideslopeError>0.5 then -- "You're a little high." - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.HIGH, false) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.HIGH, false) advice=advice+AIRBOSS.LSOCall.HIGH.duration elseif glideslopeError<-1.0 then -- "Power!" - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.POWER, true) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.POWER, true) advice=advice+AIRBOSS.LSOCall.POWER.duration elseif glideslopeError<-0.5 then -- "You're a little low." - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.POWER, false) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.POWER, false) advice=advice+AIRBOSS.LSOCall.POWER.duration else text="Good altitude." @@ -4881,19 +5092,19 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- Lineup left/right calls. if lineupError<-3 then -- "Come left!" - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.COMELEFT, true) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.COMELEFT, true) advice=advice+AIRBOSS.LSOCall.COMELEFT.duration elseif lineupError<-1 then -- "Come left." - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.COMELEFT, false) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.COMELEFT, false) advice=advice+AIRBOSS.LSOCall.COMELEFT.duration elseif lineupError>3 then -- "Right for lineup!" - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.RIGHTFORLINEUP, true) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RIGHTFORLINEUP, true) advice=advice+AIRBOSS.LSOCall.RIGHTFORLINEUP.duration elseif lineupError>1 then -- "Right for lineup." - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.RIGHTFORLINEUP, false) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RIGHTFORLINEUP, false) advice=advice+AIRBOSS.LSOCall.RIGHTFORLINEUP.duration else text=text.."Good lineup." @@ -4910,21 +5121,21 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- Rate aoa. if aoa>=aircraftaoa.Slow then -- "Your're slow!" - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.SLOW, true) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.SLOW, true) advice=advice+AIRBOSS.LSOCall.SLOW.duration elseif aoa>=aircraftaoa.OnSpeedMax and aoa=aircraftaoa.OnSpeedMin and aoa=aircraftaoa.Fast and aoa24 seconds: No Grade + ]] if playerData.patternwo or playerData.waveoff then grade="CUT" @@ -5124,7 +5344,7 @@ function AIRBOSS:_Flightdata2Text(fdata) text=text..string.format("LUE=%.1f\n",LUE) text=text..string.format("ROL=%.1f\n",ROL) text=text..G - self:T(self.lid..text) + self:T3(self.lid..text) return G,n end @@ -5543,8 +5763,11 @@ function AIRBOSS:_Debrief(playerData) table.insert(playerData.grades, mygrade) -- LSO grade message. - local text=string.format("%s %.1f PT - %s\n", grade, points, analysis) - text=text..string.format("Your detailed debriefing can be found via the F10 radio menu.") + local text=string.format("%s %.1f PT - %s", grade, points, analysis) + if playerData.wire then + text=text..string.format(" %d-wire", playerData.wire) + end + text=text..string.format("\nYour detailed debriefing can be found via the F10 radio menu.") self:MessageToPlayer(playerData, text, "LSO", "", 30, true) -- Check if boltered or waved off? @@ -5560,6 +5783,8 @@ function AIRBOSS:_Debrief(playerData) if playerData.unit:IsAlive() then + -- TODO: handle case where player landed even though he was waved off! + -- Heading and distance tip. local heading, distance @@ -5981,7 +6206,7 @@ end -- @param #boolean loud If true, play loud sound file version. -- @param #number delay Delay in seconds, before the message is broadcasted. function AIRBOSS:RadioTransmission(radio, call, loud, delay) - self:E({radio=radio, call=call, loud=loud, delay=delay}) + self:F2({radio=radio, call=call, loud=loud, delay=delay}) -- Create a new radio transmission item. local transmission={} --#AIRBOSS.Radioitem @@ -6086,9 +6311,9 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration if receiver==playerData.onboard and not soundoff then if sender then if sender=="LSO" then - self:_Number2Sound(self.LSOradio, receiver, delay) + self:_Number2Sound(self.LSORadio, receiver, delay) elseif sender=="MARSHAL" then - self:_Number2Sound(self.Carrierradio, receiver, delay) + self:_Number2Sound(self.MarshalRadio, receiver, delay) end end end @@ -6130,10 +6355,10 @@ function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay, -- Check who is the sender. if sender=="LSO" then -- Sender is LSO or AIRBOSS ==> Broadcast on LSO radio. - self:_Number2Sound(self.LSOradio, receiver, delay) + self:_Number2Sound(self.LSORadio, receiver, delay) elseif sender=="MARSHAL" then -- Sender is MARSHAL ==> Broadcast on MARSHAL radio. - self:_Number2Sound(self.Carrierradio, receiver, delay) + self:_Number2Sound(self.MarshalRadio, receiver, delay) end playit=false -- Play only once, in case two have the same flight number. end @@ -6252,7 +6477,9 @@ function AIRBOSS:_AddF10Commands(_unitName) -- F10/Airboss/ local _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) - -- F10/Airboss//Help + -------------------------------- + -- F10/Airboss//F1 Help + -------------------------------- local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) -- F10/Airboss//Help/Skill Level local _skillPath=missionCommands.addSubMenuForGroup(gid, "Skill Level", _helpPath) @@ -6273,8 +6500,9 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(gid, "Attitude Monitor ON/OFF", _helpPath, self._AttitudeMonitor, self, playername) missionCommands.addCommandForGroup(gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) - - -- F10/Airboss//Kneeboard + ------------------------------------- + -- F10/Airboss//F2 Kneeboard -- + ------------------------------------- local _kneeboardPath=missionCommands.addSubMenuForGroup(gid, "Kneeboard", _rootPath) -- F10/Airboss//Kneeboard/Results local _resultsPath=missionCommands.addSubMenuForGroup(gid, "Results", _kneeboardPath) @@ -6286,13 +6514,14 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(gid, "Carrier Info", _kneeboardPath, self._DisplayCarrierInfo, self, _unitName) missionCommands.addCommandForGroup(gid, "Weather Report", _kneeboardPath, self._DisplayCarrierWeather, self, _unitName) missionCommands.addCommandForGroup(gid, "My Status", _kneeboardPath, self._DisplayPlayerStatus, self, _unitName) + missionCommands.addCommandForGroup(gid, "Set Section", _kneeboardPath, self._SetSection, self, _unitName) - - -- F10/Airboss// + ---------------------------- + -- F10/Airboss// -- + ---------------------------- missionCommands.addCommandForGroup(gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) - missionCommands.addCommandForGroup(gid, "Request Commencing", _rootPath, self._RequestCommence, self, _unitName) - missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) - missionCommands.addCommandForGroup(gid, "Set Section", _rootPath, self._SetSection, self, _unitName) + missionCommands.addCommandForGroup(gid, "Request Commence", _rootPath, self._RequestCommence, self, _unitName) + missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) end else self:T(self.lid.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) @@ -6355,7 +6584,7 @@ function AIRBOSS:_LSORadioCheck(_unitName) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then -- Broadcase LSO radio check message on LSO radio. - self:RadioTransmission(self.LSOradio, AIRBOSS.LSOCall.RADIOCHECK) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RADIOCHECK) end end end @@ -6374,7 +6603,7 @@ function AIRBOSS:_MarshalRadioCheck(_unitName) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then -- Broadcase Marshal radio check message on Marshal radio. - self:RadioTransmission(self.Carrierradio, AIRBOSS.MarshalCall.RADIOCHECK) + self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.RADIOCHECK) end end end @@ -6484,7 +6713,7 @@ function AIRBOSS:_RequestCommence(_unitName) local _,npattern=self:_GetQueueInfo(self.Qpattern) -- Check if pattern is already full. - if npattern>self.Nmaxpattern then + if npattern>=self.Nmaxpattern then -- Patern is full! text=string.format("Negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern) else @@ -6898,8 +7127,8 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) text=text..string.format("BRC %03d°\n", self:GetBRC()) text=text..string.format("FB %03d°\n", self:GetFinalBearing(true)) text=text..string.format("Speed %d kts\n", carrierspeed) - text=text..string.format("Marshal radio %.3f MHz\n", self.Carrierfreq) --TODO: add modulation - text=text..string.format("LSO radio %.3f MHz\n", self.LSOfreq) + text=text..string.format("Marshal radio %.3f MHz\n", self.MarshalFreq) --TODO: add modulation + text=text..string.format("LSO radio %.3f MHz\n", self.LSOFreq) text=text..string.format("TACAN Channel %s\n", tacan) text=text..string.format("ICLS Channel %s\n", icls) text=text..string.format("# A/C total %d\n", #self.flights) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index f6c5d596c..4f7adaf8f 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -1114,7 +1114,7 @@ end --- Check if heading or position have changed significantly. -- @param #RECOVERYTANKER self -- @param #number dt Time since last update in seconds. --- @return #boolean If true, heading and/or position have changed more than 10 degrees or 10 km, respectively. +-- @return #boolean If true, heading and/or position have changed more than 5 degrees or 10 km, respectively. function RECOVERYTANKER:_CheckPatternUpdate(dt) -- Get current position and orientation of carrier. From 9795d5655f89ae3a248c8f11caf60217496e32ba Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 9 Dec 2018 12:01:15 +0100 Subject: [PATCH 085/147] Improvements and Fixes from FF/Develop --- Moose Development/Moose/AI/AI_Formation.lua | 44 +- Moose Development/Moose/Core/Point.lua | 34 +- Moose Development/Moose/Core/Radio.lua | 431 +++++++++++++----- Moose Development/Moose/Core/Set.lua | 4 +- Moose Development/Moose/Core/SpawnStatic.lua | 43 ++ Moose Development/Moose/Core/UserFlag.lua | 2 +- Moose Development/Moose/Core/UserSound.lua | 12 +- Moose Development/Moose/Core/Zone.lua | 43 +- .../Moose/Functional/Artillery.lua | 182 ++++---- .../Moose/Functional/Detection.lua | 2 +- Moose Development/Moose/Functional/RAT.lua | 23 +- Moose Development/Moose/Functional/Range.lua | 27 +- Moose Development/Moose/Utilities/Utils.lua | 110 ++++- .../Moose/Wrapper/Controllable.lua | 190 +++++++- Moose Development/Moose/Wrapper/Group.lua | 16 +- .../Moose/Wrapper/Positionable.lua | 15 +- Moose Development/Moose/Wrapper/Unit.lua | 22 +- 17 files changed, 911 insertions(+), 289 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 489e69cf3..c02096609 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 @@ -125,6 +127,7 @@ AI_FORMATION = { -- @param Wrapper.Unit#UNIT FollowUnit The UNIT leading the FolllowGroupSet. -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. -- @param #string FollowName Name of the escort. +-- @param #string FollowBriefing Briefing. -- @return #AI_FORMATION self function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefing ) --R2.1 local self = BASE:Inherit( self, FSM_SET:New( FollowGroupSet ) ) @@ -139,7 +142,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 +623,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 +906,30 @@ function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 end ---- @param Follow#AI_FORMATION self +--- Stop function. Formation will not be updated any more. +-- @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: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 + return true +end + +--- @param #AI_FORMATION self function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 self:F( ) @@ -1032,8 +1068,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/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 528ac9a84..366ad03e1 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -342,18 +342,18 @@ do -- COORDINATE return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z end - --- Returns if the 2 coordinates are at the same 2D position. + --- Scan/find objects (units, statics, scenery) within a certain radius around the coordinate using the world.searchObjects() DCS API function. -- @param #COORDINATE self -- @param #number radius (Optional) Scan radius in meters. Default 100 m. -- @param #boolean scanunits (Optional) If true scan for units. Default true. -- @param #boolean scanstatics (Optional) If true scan for static objects. Default true. -- @param #boolean scanscenery (Optional) If true scan for scenery objects. Default false. - -- @return True if units were found. - -- @return True if statics were found. - -- @return True if scenery objects were found. - -- @return Unit objects found. - -- @return Static objects found. - -- @return Scenery objects found. + -- @return #boolean True if units were found. + -- @return #boolean True if statics were found. + -- @return #boolean True if scenery objects were found. + -- @return #table Table of MOOSE @[#Wrapper.Unit#UNIT} objects found. + -- @return #table Table of DCS static objects found. + -- @return #table Table of DCS scenery objects found. function COORDINATE:ScanObjects(radius, scanunits, scanstatics, scanscenery) self:F(string.format("Scanning in radius %.1f m.", radius)) @@ -405,18 +405,17 @@ do -- COORDINATE local ObjectCategory = ZoneObject:getCategory() -- Check for unit or static objects - --if (ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive()) then - if (ObjectCategory == Object.Category.UNIT and ZoneObject:isExist()) then + if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() then table.insert(Units, UNIT:Find(ZoneObject)) gotunits=true - elseif (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then table.insert(Statics, ZoneObject) gotstatics=true - elseif ObjectCategory == Object.Category.SCENERY then + elseif ObjectCategory==Object.Category.SCENERY then table.insert(Scenery, ZoneObject) gotscenery=true @@ -460,12 +459,12 @@ do -- COORDINATE --- Add a Distance in meters from the COORDINATE orthonormal plane, with the given angle, and calculate the new COORDINATE. -- @param #COORDINATE self -- @param DCS#Distance Distance The Distance to be added in meters. - -- @param DCS#Angle Angle The Angle in degrees. - -- @return #COORDINATE The new calculated COORDINATE. + -- @param DCS#Angle Angle The Angle in degrees. Defaults to 0 if not specified (nil). + -- @return Core.Point#COORDINATE The new calculated COORDINATE. function COORDINATE:Translate( Distance, Angle ) local SX = self.x local SY = self.z - local Radians = Angle / 180 * math.pi + local Radians = (Angle or 0) / 180 * math.pi local TX = Distance * math.cos( Radians ) + SX local TY = Distance * math.sin( Radians ) + SY @@ -1121,6 +1120,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 +1131,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, nil, airbase, DCSTasks, description) end diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 954fc51a9..662b7b94f 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -9,12 +9,12 @@ -- -- The Radio contains 2 classes : RADIO and BEACON -- --- What are radio communications in DCS ? +-- What are radio communications in DCS? -- -- * Radio transmissions consist of **sound files** that are broadcasted on a specific **frequency** (e.g. 115MHz) and **modulation** (e.g. AM), -- * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmiter's antenna can be set, and the transmission can be **looped**. -- --- How to supply DCS my own Sound Files ? +-- How to supply DCS my own Sound Files? -- -- * Your sound files need to be encoded in **.ogg** or .wav, -- * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings, @@ -23,7 +23,7 @@ -- -- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or by any other @{Wrapper.Positionable#POSITIONABLE} -- --- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, +-- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, -- * If the transmitter is any other @{Wrapper.Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped. -- -- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft, @@ -33,7 +33,7 @@ -- -- === -- --- ### Author: Hugues "Grey_Echo" Bousquet +-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky -- -- @module Core.Radio -- @image Core_Radio.JPG @@ -66,24 +66,25 @@ -- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts -- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call -- --- What is this power thing ? +-- What is this power thing? -- -- * If your transmission is sent by a @{Wrapper.Positionable#POSITIONABLE} other than a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, you can set the power of the antenna, -- * Otherwise, DCS sets it automatically, depending on what's available on your Unit, --- * If the player gets **too far** from the transmiter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, +-- * If the player gets **too far** from the transmitter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, -- * This an automated DCS calculation you have no say on, --- * For reference, a standard VOR station has a 100W antenna, a standard AA TACAN has a 120W antenna, and civilian ATC's antenna usually range between 300 and 500W, +-- * For reference, a standard VOR station has a 100 W antenna, a standard AA TACAN has a 120 W antenna, and civilian ATC's antenna usually range between 300 and 500 W, -- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. -- -- @type RADIO --- @field Positionable#POSITIONABLE Positionable The transmiter --- @field #string FileName Name of the sound file --- @field #number Frequency Frequency of the transmission in Hz --- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM) --- @field #string Subtitle Subtitle of the transmission --- @field #number SubtitleDuration Duration of the Subtitle in seconds --- @field #number Power Power of the antenna is Watts --- @field #boolean Loop (default true) +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will transmit the radio calls. +-- @field #string FileName Name of the sound file played. +-- @field #number Frequency Frequency of the transmission in Hz. +-- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM). +-- @field #string Subtitle Subtitle of the transmission. +-- @field #number SubtitleDuration Duration of the Subtitle in seconds. +-- @field #number Power Power of the antenna is Watts. +-- @field #boolean Loop Transmission is repeated (default true). +-- @field #string alias Name of the radio transmitter. -- @extends Core.Base#BASE RADIO = { ClassName = "RADIO", @@ -93,19 +94,19 @@ RADIO = { Subtitle = "", SubtitleDuration = 0, Power = 100, - Loop = true, + Loop = false, + alias=nil, } ---- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast --- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead +--- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast. +-- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead. -- @param #RADIO self -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #RADIO Radio --- @return #nil If Positionable is invalid +-- @return #RADIO The RADIO object or #nil if Positionable is invalid. function RADIO:New(Positionable) + + -- Inherit base local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO - - self.Loop = true -- default Loop to true (not sure the above RADIO definition actually is working) self:F(Positionable) if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid @@ -113,11 +114,27 @@ function RADIO:New(Positionable) return self end - self:E({"The passed positionable is invalid, no RADIO created", Positionable}) + self:E({error="The passed positionable is invalid, no RADIO created!", positionable=Positionable}) return nil end ---- Check validity of the filename passed and sets RADIO.FileName +--- Set alias of the transmitter. +-- @param #RADIO self +-- @param #string alias Name of the radio transmitter. +-- @return #RADIO self +function RADIO:SetAlias(alias) + self.alias=tostring(alias) + return self +end + +--- Get alias of the transmitter. +-- @param #RADIO self +-- @return #string Name of the transmitter. +function RADIO:GetAlias() + return tostring(self.alias) +end + +--- Set the file name for the radio transmission. -- @param #RADIO self -- @param #string FileName File name of the sound file (i.e. "Noise.ogg") -- @return #RADIO self @@ -125,49 +142,63 @@ function RADIO:SetFileName(FileName) self:F2(FileName) if type(FileName) == "string" then + if FileName:find(".ogg") or FileName:find(".wav") then if not FileName:find("l10n/DEFAULT/") then FileName = "l10n/DEFAULT/" .. FileName end + self.FileName = FileName return self end end - self:E({"File name invalid. Maybe something wrong with the extension ?", self.FileName}) + self:E({"File name invalid. Maybe something wrong with the extension?", FileName}) return self end ---- Check validity of the frequency passed and sets RADIO.Frequency +--- Set the frequency for the radio transmission. +-- If the transmitting positionable is a unit or group, this also set the command "SetFrequency" with the defined frequency and modulation. -- @param #RADIO self --- @param #number Frequency in MHz (Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz) +-- @param #number Frequency Frequency in MHz. Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz. -- @return #RADIO self function RADIO:SetFrequency(Frequency) self:F2(Frequency) + if type(Frequency) == "number" then + -- If frequency is in range if (Frequency >= 30 and Frequency < 88) or (Frequency >= 108 and Frequency < 152) or (Frequency >= 225 and Frequency < 400) then - self.Frequency = Frequency * 1000000 -- Conversion in Hz + + -- Convert frequency from MHz to Hz + self.Frequency = Frequency * 1000000 + -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ + + local commandSetFrequency={ id = "SetFrequency", params = { - frequency = self.Frequency, + frequency = self.Frequency, modulation = self.Modulation, } - }) + } + + self:T2(commandSetFrequency) + self.Positionable:SetCommand(commandSetFrequency) end + return self end end - self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", self.Frequency}) + + self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", Frequency}) return self end ---- Check validity of the frequency passed and sets RADIO.Modulation +--- Set AM or FM modulation of the radio transmitter. -- @param #RADIO self --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM +-- @param #number Modulation Modulation is either radio.modulation.AM or radio.modulation.FM. -- @return #RADIO self function RADIO:SetModulation(Modulation) self:F2(Modulation) @@ -183,23 +214,24 @@ end --- Check validity of the power passed and sets RADIO.Power -- @param #RADIO self --- @param #number Power in W +-- @param #number Power Power in W. -- @return #RADIO self function RADIO:SetPower(Power) self:F2(Power) + if type(Power) == "number" then self.Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that - return self + else + self:E({"Power is invalid. Power unchanged.", self.Power}) end - self:E({"Power is invalid. Power unchanged.", self.Power}) + return self end ---- Check validity of the loop passed and sets RADIO.Loop +--- Set message looping on or off. -- @param #RADIO self --- @param #boolean Loop +-- @param #boolean Loop If true, message is repeated indefinitely. -- @return #RADIO self --- @usage function RADIO:SetLoop(Loop) self:F2(Loop) if type(Loop) == "boolean" then @@ -232,13 +264,12 @@ function RADIO:SetSubtitle(Subtitle, SubtitleDuration) self:E({"Subtitle is invalid. Subtitle reset.", self.Subtitle}) end if type(SubtitleDuration) == "number" then - if math.floor(math.abs(SubtitleDuration)) == SubtitleDuration then - self.SubtitleDuration = SubtitleDuration - return self - end + self.SubtitleDuration = SubtitleDuration + else + self.SubtitleDuration = 0 + self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) end - self.SubtitleDuration = 0 - self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) + return self end --- Create a new transmission, that is to say, populate the RADIO with relevant data @@ -246,10 +277,10 @@ end -- but it will work with a UNIT or a GROUP anyway. -- Only the #RADIO and the Filename are mandatory -- @param #RADIO self --- @param #string FileName --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #number Power in W +-- @param #string FileName Name of the sound file that will be transmitted. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation of frequency, which is either radio.modulation.AM or radio.modulation.FM. +-- @param #number Power Power in W. -- @return #RADIO self function RADIO:NewGenericTransmission(FileName, Frequency, Modulation, Power, Loop) self:F({FileName, Frequency, Modulation, Power}) @@ -269,31 +300,43 @@ end -- but it will work for any @{Wrapper.Positionable#POSITIONABLE}. -- Only the RADIO and the Filename are mandatory. -- @param #RADIO self --- @param #string FileName --- @param #string Subtitle --- @param #number SubtitleDuration in s --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #boolean Loop +-- @param #string FileName Name of sound file. +-- @param #string Subtitle Subtitle to be displayed with sound file. +-- @param #number SubtitleDuration Duration of subtitle display in seconds. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation which can be either radio.modulation.AM or radio.modulation.FM +-- @param #boolean Loop If true, loop message. -- @return #RADIO self function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop) self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) + -- Set file name. self:SetFileName(FileName) - local Duration = 5 - if SubtitleDuration then Duration = SubtitleDuration end - -- SubtitleDuration argument was missing, adding it - if Subtitle then self:SetSubtitle(Subtitle, Duration) end - -- self:SetSubtitleDuration is non existent, removing faulty line - -- if SubtitleDuration then self:SetSubtitleDuration(SubtitleDuration) end - if Frequency then self:SetFrequency(Frequency) end - if Modulation then self:SetModulation(Modulation) end - if Loop then self:SetLoop(Loop) end + + -- Set modulation AM/FM. + if Modulation then + self:SetModulation(Modulation) + end + + -- Set frequency. + if Frequency then + self:SetFrequency(Frequency) + end + + -- Set subtitle. + if Subtitle then + self:SetSubtitle(Subtitle, SubtitleDuration or 0) + end + + -- Set Looping. + if Loop then + self:SetLoop(Loop) + end return self end ---- Actually Broadcast the transmission +--- Broadcast the transmission. -- * The Radio has to be populated with the new transmission before broadcasting. -- * Please use RADIO setters or either @{#RADIO.NewGenericTransmission} or @{#RADIO.NewUnitTransmission} -- * This class is in fact pretty smart, it determines the right DCS function to use depending on the type of POSITIONABLE @@ -302,31 +345,38 @@ end -- * If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored. -- * If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration are ignored -- @param #RADIO self +-- @param #boolean viatrigger Use trigger.action.radioTransmission() in any case, i.e. also for UNITS and GROUPS. -- @return #RADIO self -function RADIO:Broadcast() - self:F() +function RADIO:Broadcast(viatrigger) + self:F({viatrigger=viatrigger}) - -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self:T2("Broadcasting from a UNIT or a GROUP") - self.Positionable:SetCommand({ + -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system. + if (self.Positionable.ClassName=="UNIT" or self.Positionable.ClassName=="GROUP") and (not viatrigger) then + self:T("Broadcasting from a UNIT or a GROUP") + + local commandTransmitMessage={ id = "TransmitMessage", params = { file = self.FileName, duration = self.SubtitleDuration, subtitle = self.Subtitle, loop = self.Loop, - } - }) + }} + + self:T3(commandTransmitMessage) + self.Positionable:SetCommand(commandTransmitMessage) else -- If the POSITIONABLE is anything else, we revert to the general singleton function -- I need to give it a unique name, so that the transmission can be stopped later. I use the class ID - self:T2("Broadcasting from a POSITIONABLE") + self:T("Broadcasting from a POSITIONABLE") trigger.action.radioTransmission(self.FileName, self.Positionable:GetPositionVec3(), self.Modulation, self.Loop, self.Frequency, self.Power, tostring(self.ID)) end + return self end + + --- Stops a transmission -- This function is especially usefull to stop the broadcast of looped transmissions -- @param #RADIO self @@ -335,10 +385,10 @@ function RADIO:StopBroadcast() self:F() -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ - id = "StopTransmission", - params = {} - }) + + local commandStopTransmission={id="StopTransmission", params={}} + + self.Positionable:SetCommand(commandStopTransmission) else -- Else, we use the appropriate singleton funciton trigger.action.stopRadioTransmission(tostring(self.ID)) @@ -364,22 +414,86 @@ end -- Use @{#BEACON:StopRadioBeacon}() to stop it. -- -- @type BEACON +-- @field #string ClassName Name of the class "BEACON". +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities. -- @extends Core.Base#BASE BEACON = { ClassName = "BEACON", + Positionable = nil, } ---- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.AATACAN} or @{#BEACON.Generic} +--- Beacon types supported by DCS. +-- @type BEACON.Type +-- @field #number NULL +-- @field #number VOR +-- @field #number DME +-- @field #number VOR_DME +-- @field #number TACAN +-- @field #number VORTAC +-- @field #number RSBN +-- @field #number BROADCAST_STATION +-- @field #number HOMER +-- @field #number AIRPORT_HOMER +-- @field #number AIRPORT_HOMER_WITH_MARKER +-- @field #number ILS_FAR_HOMER +-- @field #number ILS_NEAR_HOMER +-- @field #number ILS_LOCALIZER +-- @field #number ILS_GLIDESLOPE +-- @field #number NAUTICAL_HOMER +-- @field #number ICLS +BEACON.Type={ + NULL = 0, + VOR = 1, + DME = 2, + VOR_DME = 3, + TACAN = 4, + VORTAC = 5, + RSBN = 32, + BROADCAST_STATION = 1024, + HOMER = 8, + AIRPORT_HOMER = 4104, + AIRPORT_HOMER_WITH_MARKER = 4136, + ILS_FAR_HOMER = 16408, + ILS_NEAR_HOMER = 16456, + ILS_LOCALIZER = 16640, + ILS_GLIDESLOPE = 16896, + NAUTICAL_HOMER = 32776, + ICLS = 131584, +} + +--- Beacon systems supported by DCS. +-- @type BEACON.System +-- @field #number PAR_10 +-- @field #number RSBN_5 +-- @field #number TACAN +-- @field #number TACAN_TANKER +-- @field #number ILS_LOCALIZER +-- @field #number ILS_GLIDESLOPE +-- @field #number BROADCAST_STATION +BEACON.System={ + PAR_10 = 1, + RSBN_5 = 2, + TACAN = 3, + TACAN_TANKER = 4, + ILS_LOCALIZER = 5, + ILS_GLIDESLOPE = 6, + BROADCAST_STATION = 7, +} + +--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc. -- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead. -- @param #BEACON self -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #BEACON Beacon --- @return #nil If Positionable is invalid +-- @return #BEACON Beacon object or #nil if the positionable is invalid. function BEACON:New(Positionable) - local self = BASE:Inherit(self, BASE:New()) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) --#BEACON + -- Debug. self:F(Positionable) + -- Set positionable. if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid self.Positionable = Positionable return self @@ -390,44 +504,95 @@ function BEACON:New(Positionable) end ---- Converts a TACAN Channel/Mode couple into a frequency in Hz +--- Activates a TACAN BEACON. -- @param #BEACON self --- @param #number TACANChannel --- @param #string TACANMode --- @return #number Frequecy --- @return #nil if parameters are invalid -function BEACON:_TACANToFrequency(TACANChannel, TACANMode) - self:F3({TACANChannel, TACANMode}) - - if type(TACANChannel) ~= "number" then - if TACANMode ~= "X" and TACANMode ~= "Y" then - return nil -- error in arguments - end +-- @param #number Channel TACAN channel, i.e. the "10" part in "10Y". +-- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y". +-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a TACAN Beacon for a tanker +-- local myUnit = UNIT:FindByName("MyUnit") +-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon +-- +-- myBeacon:TACAN(20, "Y", "TEXACO", true) -- Activate the beacon +function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) + self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) + + -- Get frequency. + local Frequency=UTILS.TACANToFrequency(Channel, Mode) + + -- Check. + if not Frequency then + self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) + return self end --- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. --- I have no idea what it does but it seems to work - local A = 1151 -- 'X', channel >= 64 - local B = 64 -- channel >= 64 + -- Beacon type. + local Type=BEACON.Type.TACAN - if TACANChannel < 64 then - B = 1 - end + -- Beacon system. + local System=BEACON.System.TACAN - if TACANMode == 'Y' then - A = 1025 - if TACANChannel < 64 then - A = 1088 - end - else -- 'X' - if TACANChannel < 64 then - A = 962 + -- Check if unit is an aircraft and set system accordingly. + local AA=self.Positionable:IsAir() + if AA then + System=BEACON.System.TACAN_TANKER + -- Check if "Y" mode is selected for aircraft. + if Mode~="Y" then + self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable}) end end - return (A + TACANChannel - B) * 1000000 + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug. + self:T({"TACAN BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing) + + -- Stop sheduler. + if Duration then + self.Positionable:DeactivateBeacon(Duration) + end + + return self end +--- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system. +-- @param #BEACON self +-- @param #number Channel ICLS channel. +-- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +function BEACON:ActivateICLS(Channel, Callsign, Duration) + self:F({Channel=Channel, Callsign=Callsign, Duration=Duration}) + + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug + self:T2({"ICLS BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign) + + -- Stop sheduler + if Duration then -- Schedule the stop of the BEACON if asked by the MD + self.Positionable:DeactivateBeacon(Duration) + end + + return self +end + + + + + --- Activates a TACAN BEACON on an Aircraft. -- @param #BEACON self @@ -480,7 +645,7 @@ function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) }) if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD - SCHEDULER:New( nil, + SCHEDULER:New(nil, function() self:StopAATACAN() end, {}, BeaconDuration) @@ -591,4 +756,44 @@ function BEACON:StopRadioBeacon() self:F() -- The unique name of the transmission is the class ID trigger.action.stopRadioTransmission(tostring(self.ID)) -end \ No newline at end of file + return self +end + +--- Converts a TACAN Channel/Mode couple into a frequency in Hz +-- @param #BEACON self +-- @param #number TACANChannel +-- @param #string TACANMode +-- @return #number Frequecy +-- @return #nil if parameters are invalid +function BEACON:_TACANToFrequency(TACANChannel, TACANMode) + self:F3({TACANChannel, TACANMode}) + + if type(TACANChannel) ~= "number" then + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end + diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 0864fb6c1..6acb294ab 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -409,9 +409,9 @@ do -- SET_BASE for ObjectID, ObjectData in pairs( self.Set ) do if NearestObject == nil then NearestObject = ObjectData - ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() ) + ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) else - local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() ) + local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) if Distance < ClosestDistance then NearestObject = ObjectData ClosestDistance = Distance diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index 0081195a5..e5a7b4e59 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -195,6 +195,49 @@ function SPAWNSTATIC:SpawnFromPointVec2( PointVec2, Heading, NewName ) --R2.1 end +--- Creates a new @{Static} from a COORDINATE. +-- @param #SPAWNSTATIC self +-- @param Core.Point#COORDINATE Coordinate The 3D coordinate where to spawn the static. +-- @param #number Heading (Optional) Heading The heading of the static, which is a number in degrees from 0 to 360. Default is 0 degrees. +-- @param #string NewName (Optional) The name of the new static. +-- @return #SPAWNSTATIC +function SPAWNSTATIC:SpawnFromCoordinate(Coordinate, Heading, NewName) --R2.4 + self:F( { PointVec2, Heading, NewName } ) + + local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix ) + + if StaticTemplate then + + Heading=Heading or 0 + + local StaticUnitTemplate = StaticTemplate.units[1] + + StaticUnitTemplate.x = Coordinate.x + StaticUnitTemplate.y = Coordinate.z + StaticUnitTemplate.alt = Coordinate.y + + StaticTemplate.route = nil + StaticTemplate.groupId = nil + + StaticTemplate.name = NewName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex ) + StaticUnitTemplate.name = StaticTemplate.name + StaticUnitTemplate.heading = ( Heading / 180 ) * math.pi + + _DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID) + + self:F({StaticTemplate = StaticTemplate}) + + local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] ) + + self.SpawnIndex = self.SpawnIndex + 1 + + return _DATABASE:FindStatic(Static:getName()) + end + + return nil +end + + --- Respawns the original @{Static}. -- @param #SPAWNSTATIC self -- @return #SPAWNSTATIC diff --git a/Moose Development/Moose/Core/UserFlag.lua b/Moose Development/Moose/Core/UserFlag.lua index 88c1d0f60..bef5cefff 100644 --- a/Moose Development/Moose/Core/UserFlag.lua +++ b/Moose Development/Moose/Core/UserFlag.lua @@ -70,7 +70,7 @@ do -- UserFlag -- local BlueVictory = USERFLAG:New( "VictoryBlue" ) -- local BlueVictoryValue = BlueVictory:Get() -- Get the UserFlag VictoryBlue value. -- - function USERFLAG:Get( Number ) --R2.3 + function USERFLAG:Get() --R2.3 return trigger.misc.getUserFlag( self.UserFlagName ) end diff --git a/Moose Development/Moose/Core/UserSound.lua b/Moose Development/Moose/Core/UserSound.lua index a0547a5cf..b0f6fb393 100644 --- a/Moose Development/Moose/Core/UserSound.lua +++ b/Moose Development/Moose/Core/UserSound.lua @@ -118,15 +118,21 @@ do -- UserSound --- Play the usersound to the given @{Wrapper.Group}. -- @param #USERSOUND self -- @param Wrapper.Group#GROUP Group The @{Wrapper.Group} to play the usersound to. + -- @param #number Delay (Optional) Delay in seconds, before the sound is played. Default 0. -- @return #USERSOUND The usersound instance. -- @usage -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) -- local PlayerGroup = GROUP:FindByName( "PlayerGroup" ) -- Search for the active group named "PlayerGroup", that contains a human player. -- BlueVictory:ToGroup( PlayerGroup ) -- Play the sound that Blue has won to the player group. -- - function USERSOUND:ToGroup( Group ) --R2.3 - - trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName ) + function USERSOUND:ToGroup( Group, Delay ) --R2.3 + + Delay=Delay or 0 + if Delay>0 then + SCHEDULER:New(nil, USERSOUND.ToGroup,{self, Group}, Delay) + else + trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName ) + end return self end diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 9b5b6827f..e0971ee06 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -1398,16 +1398,15 @@ end --- Smokes the zone boundaries in a color. -- @param #ZONE_POLYGON_BASE self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. -- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) +function ZONE_POLYGON_BASE:SmokeZone( SmokeColor, Segments ) self:F2( SmokeColor ) - local i - local j - local Segments = 10 + Segments=Segments or 10 - i = 1 - j = #self._.Polygon + local i=1 + local j=#self._.Polygon while i <= #self._.Polygon do self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) @@ -1428,6 +1427,38 @@ function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) end +--- Flare the zone boundaries in a color. +-- @param #ZONE_POLYGON_BASE self +-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments ) + self:F2(FlareColor) + + Segments=Segments or 10 + + local i=1 + local j=#self._.Polygon + + while i <= #self._.Polygon do + self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) + + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x + local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y + + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. + local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) + POINT_VEC2:New( PointX, PointY ):Flare(FlareColor) + end + j = i + i = i + 1 + end + + return self +end + + --- Returns if a location is within the zone. diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 8509928fb..637560d03 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. --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2878,7 +2878,7 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) self.Controllable:ClearTasks() else - self:E(ARTY.id.."ERROR: No target in cease fire for group %s.", self.groupname) + self:E(ARTY.id..string.format("ERROR: No target in cease fire for group %s.", self.groupname)) end -- Set number of shots to zero. @@ -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/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 7402729ab..fd3029b5a 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1012,7 +1012,7 @@ do -- DETECTION_BASE --- Set the parameters to calculate to optimal intercept point. -- @param #DETECTION_BASE self -- @param #boolean Intercept Intercept is true if an intercept point is calculated. Intercept is false if it is disabled. The default Intercept is false. - -- @param #number IntereptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. + -- @param #number InterceptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. -- @return #DETECTION_BASE self function DETECTION_BASE:SetIntercept( Intercept, InterceptDelay ) self:F2() 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/Range.lua b/Moose Development/Moose/Functional/Range.lua index 95b800521..394f134f2 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.3" --TODO list: --TODO: Add custom weapons, which can be specified by the user. @@ -460,9 +460,10 @@ function RANGE:SetBombtrackThreshold(distance) self.BombtrackThreshold=distance*1000 or 25*1000 end ---- Set range location. If this is not done, one (random) unit position of the range is used to determine the center of the range. +--- Set range location. If this is not done, one (random) unit position of the range is used to determine the location of the range. +-- The range location determines the position at which the weather data is evaluated. -- @param #RANGE self --- @param Core.Point#COORDINATE coordinate Coordinate of the center of the range. +-- @param Core.Point#COORDINATE coordinate Coordinate of the range. function RANGE:SetRangeLocation(coordinate) self.location=coordinate end @@ -471,7 +472,7 @@ end -- If a zone is not explicitly specified, the range zone is determined by its location and radius. -- @param #RANGE self -- @param Core.Zone#ZONE zone MOOSE zone defining the range perimeters. -function RANGE:SetRangeLocation(zone) +function RANGE:SetRangeZone(zone) self.rangezone=zone end @@ -1163,11 +1164,19 @@ function RANGE:OnEventShot(EventData) -- Coordinate of impact point. local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) + -- Check if impact happend in range zone. + local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) + -- Distance from range. We dont want to smoke targets outside of the range. local impactdist=impactcoord:Get2DDistance(self.location) + -- Impact point of bomb. + if self.Debug then + impactcoord:MarkToAll("Bomb impact point") + end + -- Smoke impact point of bomb. - if self.PlayerSettings[_playername].smokebombimpact and impactdist1 then @@ -680,3 +712,77 @@ function UTILS.VecCross(a, b) return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} end +--- Converts a TACAN Channel/Mode couple into a frequency in Hz. +-- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". +-- @param #string TACANMode The TACAN mode, i.e. the "X" in "10X". +-- @return #number Frequency in Hz or #nil if parameters are invalid. +function UTILS.TACANToFrequency(TACANChannel, TACANMode) + + if type(TACANChannel) ~= "number" then + return nil -- error in arguments + end + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end + + +--- Returns the DCS map/theatre as optained by env.mission.theatre +-- @return #string DCS map name . +function UTILS.GetDCSMap() + return env.mission.theatre +end + +--- Returns the magnetic declination of the map. +-- Returned values for the current maps are: +-- +-- * Caucasus +6 (East), year ~ 2011 +-- * NTTR +12 (East), year ~ 2011 +-- * Normandy -10 (West), year ~ 1944 +-- * Persian Gulf +2 (East), year ~ 2011 +-- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre +-- @return #number Declination in degrees. +function UTILS.GetMagneticDeclination(map) + + -- Map. + map=map or UTILS.GetDCSMap() + + local declination=0 + if map==DCSMAP.Caucasus then + declination=6 + elseif map==DCSMAP.NTTR then + declination=12 + elseif map==DCSMAP.Normandy then + declination=-10 + elseif map==DCSMAP.PersianGulf then + declination=2 + else + declination=0 + end + + return declination +end + + diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 1fb50baa5..91bc437a4 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -342,16 +342,26 @@ function CONTROLLABLE:PushTask( DCSTask, WaitTime ) local DCSControllable = self:GetDCSObject() if DCSControllable then - local Controller = self:_GetController() + + local DCSControllableName = self:GetName() -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. -- Therefore we schedule the functions to set the mission and options for the Controllable. - -- Controller:pushTask( DCSTask ) + -- Controller:pushTask( DCSTask ) + + local function PushTask( Controller, DCSTask ) + if self and self:IsAlive() then + local Controller = self:_GetController() + Controller:pushTask( DCSTask ) + else + BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) + end + end - if WaitTime then - self.TaskScheduler:Schedule( Controller, Controller.pushTask, { DCSTask }, WaitTime ) + if not WaitTime or WaitTime == 0 then + PushTask( self, DCSTask ) else - Controller:pushTask( DCSTask ) + self.TaskScheduler:Schedule( self, PushTask, { DCSTask }, WaitTime ) end return self @@ -362,7 +372,7 @@ end --- Clearing the Task Queue and Setting the Task on the queue from the controllable. -- @param #CONTROLLABLE self --- @param #DCS.Task DCSTask DCS Task array. +-- @param DCS#Task DCSTask DCS Task array. -- @param #number WaitTime Time in seconds, before the task is set. -- @return Wrapper.Controllable#CONTROLLABLE self function CONTROLLABLE:SetTask( DCSTask, WaitTime ) @@ -540,9 +550,9 @@ end ---- Executes a command action +--- Executes a command action for the CONTROLLABLE. -- @param #CONTROLLABLE self --- @param DCS#Command DCSCommand +-- @param DCS#Command DCSCommand The command to be executed. -- @return #CONTROLLABLE self function CONTROLLABLE:SetCommand( DCSCommand ) self:F2( DCSCommand ) @@ -630,9 +640,122 @@ function CONTROLLABLE:StartUncontrolled(delay) return self end +--- Give the CONTROLLABLE the command to activate a beacon. See [DCS_command_activateBeacon](https://wiki.hoggitworld.com/view/DCS_command_activateBeacon) on Hoggit. +-- For specific beacons like TACAN use the more convenient @{#BEACON} class. +-- Note that a controllable can only have one beacon activated at a time with the execption of ICLS. +-- @param #CONTROLLABLE self +-- @param Core.Radio#BEACON.Type Type Beacon type (VOR, DME, TACAN, RSBN, ILS etc). +-- @param Core.Radio#BEACON.System System Beacon system (VOR, DME, TACAN, RSBN, ILS etc). +-- @param #number Frequency Frequency in Hz the beacon is running on. Use @{#UTILS.TACANToFrequency} to generate a frequency for TACAN beacons. +-- @param #number UnitID The ID of the unit the beacon is attached to. Usefull if more units are in one group. +-- @param #number Channel Channel the beacon is using. For, e.g. TACAN beacons. +-- @param #string ModeChannel The TACAN mode of the beacon, i.e. "X" or "Y". +-- @param #boolean AA If true, create and Air-Air beacon. IF nil, automatically set if CONTROLLABLE depending on whether unit is and aircraft or not. +-- @param #string Callsign Morse code identification callsign. +-- @param #boolean Bearing If true, beacon provides bearing information - if supported by the unit the beacon is attached to. +-- @param #number Delay (Optional) Delay in seconds before the beacon is activated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing, Delay) + + AA=AA or self:IsAir() + UnitID=UnitID or self:GetID() + + -- Command + local CommandActivateBeacon= { + id = "ActivateBeacon", + params = { + ["type"] = Type, + ["system"] = System, + ["frequency"] = Frequency, + ["unitId"] = UnitID, + ["channel"] = Channel, + ["modeChannel"] = ModeChannel, + ["AA"] = AA, + ["callsign"] = Callsign, + ["bearing"] = Bearing, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self, Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing}, Delay) + else + self:SetCommand(CommandActivateBeacon) + end + + return self +end + +--- Activate ICLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! +-- @param #CONTROLLABLE self +-- @param #number Channel ICLS channel. +-- @param #number UnitID The ID of the unit the ICLS system is attached to. Useful if more units are in one group. +-- @param #string Callsign Morse code identification callsign. +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateICLS(Channel, UnitID, Callsign, Delay) + self:F() + + -- Command to activate ICLS system. + local CommandActivateICLS= { + id = "ActivateICLS", + params= { + ["type"] = BEACON.Type.ICLS, + ["channel"] = Channel, + ["unitId"] = UnitID, + ["callsign"] = Callsign, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateICLS, {self}, Delay) + else + self:SetCommand(CommandActivateICLS) + end + + return self +end + + +--- Deactivate the active beacon of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the beacon is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateBeacon(Delay) + self:F() + + -- Command to deactivate + local CommandDeactivateBeacon={id='DeactivateBeacon', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self}, Delay) + else + self:SetCommand(CommandDeactivateBeacon) + end + + return self +end + +--- Deactivate the ICLS of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateICLS(Delay) + self:F() + + -- Command to deactivate + local CommandDeactivateICLS={id='DeactivateICLS', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandDeactivateICLS, {self}, Delay) + else + self:SetCommand(CommandDeactivateICLS) + end + + return self +end + + -- TASKS FOR AIR CONTROLLABLES - - --- (AIR) Attack a Controllable. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. @@ -870,6 +993,38 @@ 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 + + local P1=Coord:GetVec2() + local P2=nil + if CoordRaceTrack then + Pattern=AI.Task.OrbitPattern.RACE_TRACK + P2=CoordRaceTrack:GetVec2() + end + + local Task = { + id = 'Orbit', + params = { + pattern = Pattern, + point = P1, + point2 = P2, + 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. @@ -958,11 +1113,7 @@ function CONTROLLABLE:TaskRefueling() -- params = {} -- } - local DCSTask - DCSTask = { id = 'Refueling', - params = { - }, - }, + local DCSTask={id='Refueling', params={}} self:T3( { DCSTask } ) return DCSTask @@ -2101,7 +2252,7 @@ do -- Route methods FromCoordinate = FromCoordinate or self:GetCoordinate() -- Get path and path length on road including the end points (From and To). - local PathOnRoad, LengthOnRoad=FromCoordinate:GetPathOnRoad(ToCoordinate, true) + local PathOnRoad, LengthOnRoad, GotPath =FromCoordinate:GetPathOnRoad(ToCoordinate, true) -- Get the length only(!) on the road. local _,LengthRoad=FromCoordinate:GetPathOnRoad(ToCoordinate, false) @@ -2113,7 +2264,7 @@ do -- Route methods -- Calculate the direct distance between the initial and final points. local LengthDirect=FromCoordinate:Get2DDistance(ToCoordinate) - if PathOnRoad then + if GotPath then -- Off road part of the rout: Total=OffRoad+OnRoad. LengthOffRoad=LengthOnRoad-LengthRoad @@ -2136,7 +2287,7 @@ do -- Route methods local canroad=false -- Check if a valid path on road could be found. - if PathOnRoad and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. + if GotPath and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. -- Check whether the road is very long compared to direct path. if LongRoad and Shortcut then @@ -3024,6 +3175,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..c93866343 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" ) @@ -1482,6 +1482,17 @@ function GROUP:Respawn( Template, Reset ) if not Template then Template = self:GetTemplate() end + + -- Get correct heading. + local function _Heading(course) + local h + if course<=180 then + h=math.rad(course) + else + h=-math.rad(360-course) + end + return h + end if self:IsAlive() then local Zone = self.InitRespawnZone -- Core.Zone#ZONE @@ -1515,7 +1526,8 @@ function GROUP:Respawn( Template, Reset ) Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position. Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position. - Template.units[UnitID].heading = self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading() + Template.units[UnitID].heading = _Heading(self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading()) + Template.units[UnitID].psi = -Template.units[UnitID].heading self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) end end diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index fef546f38..137d4ab4c 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -706,8 +706,8 @@ end --- Returns the unit's climb or descent angle. -- @param Wrapper.Positionable#POSITIONABLE self --- @return #number Climb or descent angle in degrees. -function POSITIONABLE:GetClimbAnge() +-- @return #number Climb or descent angle in degrees. Or 0 if velocity vector norm is zero (or nil). Or nil, if the position of the POSITIONABLE returns nil. +function POSITIONABLE:GetClimbAngle() -- Get position of the unit. local unitpos = self:GetPosition() @@ -719,10 +719,17 @@ function POSITIONABLE:GetClimbAnge() if unitvel and UTILS.VecNorm(unitvel)~=0 then - return math.asin(unitvel.y/UTILS.VecNorm(unitvel)) - + -- Calculate climb angle. + local angle=math.asin(unitvel.y/UTILS.VecNorm(unitvel)) + + -- Return angle in degrees. + return math.deg(angle) + else + return 0 end end + + return nil end --- Returns the pitch angle of a unit. diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index d24a0b0b0..d81a6c01c 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -902,29 +902,31 @@ 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 = true + -- Get DCS result of whether unit is in air or not. + local UnitInAir = DCSUnit:inAir() + + -- Get unit category. local UnitCategory = DCSUnit:getDesc().category - if UnitCategory == Unit.Category.HELICOPTER then + + -- 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. + -- This is a workaround since DCS currently does not acknoledge that helos land on buildings. + -- Note however, that the velocity check will fail if the ground is moving, e.g. on an aircraft carrier! + if UnitInAir==true and UnitCategory == Unit.Category.HELICOPTER 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 - else - UnitInAir = DCSUnit:inAir() end - - + self:T3( UnitInAir ) return UnitInAir end From afb0a8af33edf95b6f988e3474b565c52aabce83 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 9 Dec 2018 19:11:46 +0100 Subject: [PATCH 086/147] Manual diff merge from FF/Development Hopefully cleans up the mess now. --- Moose Development/Moose/AI/AI_A2A.lua | 23 +- .../Moose/AI/AI_A2A_Dispatcher.lua | 238 +- Moose Development/Moose/AI/AI_A2G.lua | 69 + .../Moose/AI/AI_A2G_Dispatcher.lua | 4146 +++++++++++++++++ Moose Development/Moose/AI/AI_A2G_Engage.lua | 440 ++ Moose Development/Moose/AI/AI_A2G_Patrol.lua | 488 ++ Moose Development/Moose/AI/AI_Air.lua | 732 +++ Moose Development/Moose/Core/Set.lua | 2156 ++++----- Moose Development/Moose/Core/Spawn.lua | 6 +- Moose Development/Moose/Core/Zone.lua | 28 +- .../Moose/Functional/Designate.lua | 26 +- .../Moose/Functional/Warehouse.lua | 128 +- .../Moose/Functional/ZoneCaptureCoalition.lua | 19 + Moose Development/Moose/Wrapper/Static.lua | 33 + Moose Setup/Moose.files | 9 + 15 files changed, 7307 insertions(+), 1234 deletions(-) create mode 100644 Moose Development/Moose/AI/AI_A2G.lua create mode 100644 Moose Development/Moose/AI/AI_A2G_Dispatcher.lua create mode 100644 Moose Development/Moose/AI/AI_A2G_Engage.lua create mode 100644 Moose Development/Moose/AI/AI_A2G_Patrol.lua create mode 100644 Moose Development/Moose/AI/AI_Air.lua diff --git a/Moose Development/Moose/AI/AI_A2A.lua b/Moose Development/Moose/AI/AI_A2A.lua index 33ee16ace..c96fa4e43 100644 --- a/Moose Development/Moose/AI/AI_A2A.lua +++ b/Moose Development/Moose/AI/AI_A2A.lua @@ -438,13 +438,14 @@ function AI_A2A:onafterStatus() RTB = false end end - - if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then - if DistanceFromHomeBase < 5000 then - self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) - self:Home( "Destroy" ) - end - end + +-- I think this code is not requirement anymore after release 2.5. +-- if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then +-- if DistanceFromHomeBase < 5000 then +-- self:E( self.Controllable:GetName() .. " is near the home base, RTB!" ) +-- self:Home( "Destroy" ) +-- end +-- end if not self:Is( "Fuel" ) and not self:Is( "Home" ) then @@ -481,9 +482,12 @@ function AI_A2A:onafterStatus() end -- Check if planes went RTB and are out of control. + -- We only check if planes are out of control, when they are in duty. if self.Controllable:HasTask() == false then if not self:Is( "Started" ) and not self:Is( "Stopped" ) and + not self:Is( "Fuel" ) and + not self:Is( "Damaged" ) and not self:Is( "Home" ) then if self.IdleCount >= 2 then if Damage ~= InitialLife then @@ -503,8 +507,11 @@ function AI_A2A:onafterStatus() if RTB == true then self:__RTB( 0.5 ) end + + if not self:Is("Home") then + self:__Status( 10 ) + end - self:__Status( 10 ) end end diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 8ba529d19..9b9370bb3 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -274,7 +274,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher_Red = AI_A2A_DISPATCHER:New( EWR_Red ) -- A2ADispatcher_Blue = AI_A2A_DISPATCHER:New( EWR_Blue ) -- - -- ### 2. Define the detected **target grouping radius**: + -- ### 1.2. Define the detected **target grouping radius**: -- -- The target grouping radius is a property of the Detection object, that was passed to the AI\_A2A\_DISPATCHER object, but can be changed. -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. @@ -1013,12 +1013,48 @@ do -- AI_A2A_DISPATCHER self:SetTacticalDisplay( false ) + self.DefenderCAPIndex = 0 + self:__Start( 5 ) return self end + --- @param #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:onafterStart( From, Event, To ) + + self:GetParent( self, AI_A2A_DISPATCHER ).onafterStart( self, From, Event, To ) + + -- Spawn the resources. + for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + DefenderSquadron.Resource = {} + if DefenderSquadron.ResourceCount then + for Resource = 1, DefenderSquadron.ResourceCount do + self:ParkDefender( DefenderSquadron ) + end + end + end + end + + + --- @param #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:ParkDefender( DefenderSquadron ) + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) + local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN + Spawn:InitGrouping( 1 ) + local SpawnGroup + if self:IsSquadronVisible( DefenderSquadron.Name ) then + SpawnGroup = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, SPAWN.Takeoff.Cold ) + local GroupName = SpawnGroup:GetName() + DefenderSquadron.Resources = DefenderSquadron.Resources or {} + DefenderSquadron.Resources[TemplateID] = DefenderSquadron.Resources[TemplateID] or {} + DefenderSquadron.Resources[TemplateID][GroupName] = {} + DefenderSquadron.Resources[TemplateID][GroupName] = SpawnGroup + end + end + + --- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2A_DISPATCHER:OnEventBaseCaptured( EventData ) @@ -1030,7 +1066,7 @@ do -- AI_A2A_DISPATCHER -- Now search for all squadrons located at the airbase, and sanatize them. for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do if Squadron.AirbaseName == AirbaseName then - Squadron.Resources = -999 -- The base has been captured, and the resources are eliminated. No more spawning. + Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. Squadron.Captured = true self:I( "Squadron " .. SquadronName .. " captured." ) end @@ -1059,6 +1095,7 @@ do -- AI_A2A_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() + self:ParkDefender( Squadron, Defender ) return end if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then @@ -1085,6 +1122,7 @@ do -- AI_A2A_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() + self:ParkDefender( Squadron, Defender ) end end end @@ -1474,7 +1512,7 @@ do -- AI_A2A_DISPATCHER -- Just remember that your template (groups late activated) need to start with the prefix you have specified in your code. -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets. -- - -- @param #number Resources (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. + -- @param #number ResourceCount (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. -- -- @usage -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. @@ -1497,13 +1535,13 @@ do -- AI_A2A_DISPATCHER -- -- @usage -- -- This is an example like the previous, but now with infinite resources. - -- -- The Resources parameter is not given in the SetSquadron method. + -- -- The ResourceCount parameter is not given in the SetSquadron method. -- A2ADispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) -- A2ADispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) -- -- -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, Resources ) + function AI_A2A_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} @@ -1528,11 +1566,11 @@ do -- AI_A2A_DISPATCHER DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate] end end - DefenderSquadron.Resources = Resources + DefenderSquadron.ResourceCount = ResourceCount DefenderSquadron.TemplatePrefixes = TemplatePrefixes DefenderSquadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured. - self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, Resources } } ) + self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) return self end @@ -1551,6 +1589,54 @@ do -- AI_A2A_DISPATCHER end + --- Set the Squadron visible before startup of the dispatcher. + -- All planes will be spawned as uncontrolled on the parking spot. + -- They will lock the parking spot. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_A2A_DISPATCHER + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- A2ADispatcher:SetSquadronVisible( "Mineralnye" ) + -- + function AI_A2A_DISPATCHER:SetSquadronVisible( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.Uncontrolled = true + + for SpawnTemplate, DefenderSpawn in pairs( self.DefenderSpawns ) do + DefenderSpawn:InitUnControlled() + end + + end + + --- Check if the Squadron is visible before startup of the dispatcher. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #bool true if visible. + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- local IsVisible = A2ADispatcher:IsSquadronVisible( "Mineralnye" ) + -- + function AI_A2A_DISPATCHER:IsSquadronVisible( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + return DefenderSquadron.Uncontrolled == true + end + + return nil + + end + --- Set a CAP for a Squadron. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -1699,7 +1785,7 @@ do -- AI_A2A_DISPATCHER if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured. - if ( not DefenderSquadron.Resources ) or ( DefenderSquadron.Resources and DefenderSquadron.Resources > 0 ) then -- And, if there are sufficient resources. + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. local Cap = DefenderSquadron.Cap if Cap then @@ -1732,7 +1818,7 @@ do -- AI_A2A_DISPATCHER if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured. - if ( not DefenderSquadron.Resources ) or ( DefenderSquadron.Resources and DefenderSquadron.Resources > 0 ) then -- And, if there are sufficient resources. + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. local Gci = DefenderSquadron.Gci if Gci then return DefenderSquadron @@ -2490,21 +2576,21 @@ do -- AI_A2A_DISPATCHER self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() self.Defenders[ DefenderName ] = Squadron - if Squadron.Resources then - Squadron.Resources = Squadron.Resources - Size + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount - Size end - self:F( { DefenderName = DefenderName, SquadronResources = Squadron.Resources } ) + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) end --- @param #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() - if Squadron.Resources then - Squadron.Resources = Squadron.Resources + Defender:GetSize() + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount + Defender:GetSize() end self.Defenders[ DefenderName ] = nil - self:F( { DefenderName = DefenderName, SquadronResources = Squadron.Resources } ) + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) end function AI_A2A_DISPATCHER:GetSquadronFromDefender( Defender ) @@ -2646,7 +2732,80 @@ do -- AI_A2A_DISPATCHER return Friendlies end + + --- + -- @param #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded ) + local SquadronName = DefenderSquadron.Name + DefendersNeeded = DefendersNeeded or 4 + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded + + if self:IsSquadronVisible( SquadronName ) then + + -- Here we CAP the new planes. + -- The Resources table is filled in advance. + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template. + + -- We determine the grouping based on the parameters set. + self:F( { DefenderGrouping = DefenderGrouping } ) + + -- New we will form the group to spawn in. + -- We search for the first free resource matching the template. + local DefenderUnitIndex = 1 + local DefenderCAPTemplate = nil + local DefenderName = nil + for GroupName, DefenderGroup in pairs( DefenderSquadron.Resources[TemplateID] or {} ) do + self:F( { GroupName = GroupName } ) + local DefenderTemplate = _DATABASE:GetGroupTemplate( GroupName ) + if DefenderUnitIndex == 1 then + DefenderCAPTemplate = UTILS.DeepCopy( DefenderTemplate ) + self.DefenderCAPIndex = self.DefenderCAPIndex + 1 + DefenderCAPTemplate.name = SquadronName .. "#" .. self.DefenderCAPIndex .. "#" .. GroupName + DefenderName = DefenderCAPTemplate.name + else + -- Add the unit in the template to the DefenderCAPTemplate. + local DefenderUnitTemplate = DefenderTemplate.units[1] + DefenderCAPTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate + end + DefenderUnitIndex = DefenderUnitIndex + 1 + DefenderSquadron.Resources[TemplateID][GroupName] = nil + if DefenderUnitIndex > DefenderGrouping then + break + end + + end + + if DefenderCAPTemplate then + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local SpawnGroup = GROUP:Register( DefenderName ) + DefenderCAPTemplate.lateActivation = nil + DefenderCAPTemplate.uncontrolled = nil + local Takeoff = self:GetSquadronTakeoff( SquadronName ) + DefenderCAPTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + DefenderCAPTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + local Defender = _DATABASE:Spawn( DefenderCAPTemplate ) + + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + return Defender, DefenderGrouping + end + else + local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN + if DefenderGrouping then + Spawn:InitGrouping( DefenderGrouping ) + else + Spawn:InitGrouping() + end + + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local Defender = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + return Defender, DefenderGrouping + end + + return nil, nil + end --- -- @param #AI_A2A_DISPATCHER self @@ -2663,15 +2822,9 @@ do -- AI_A2A_DISPATCHER local Cap = DefenderSquadron.Cap if Cap then - - local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN - local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping - Spawn:InitGrouping( DefenderGrouping ) - local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) - local DefenderCAP = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) - self:AddDefenderToSquadron( DefenderSquadron, DefenderCAP, DefenderGrouping ) - + local DefenderCAP, DefenderGrouping = self:ResourceActivate( DefenderSquadron ) + if DefenderCAP then local Fsm = AI_A2A_CAP:New( DefenderCAP, Cap.Zone, Cap.FloorAltitude, Cap.CeilingAltitude, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.AltType ) @@ -2686,7 +2839,7 @@ do -- AI_A2A_DISPATCHER self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", Fsm ) function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"GCI Birth", Defender:GetName()}) + self:F({"CAP Birth", Defender:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER @@ -2720,9 +2873,9 @@ do -- AI_A2A_DISPATCHER if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) Defender:Destroy() + self:ParkDefender( Squadron, Defender ) end end - end end end @@ -2828,31 +2981,19 @@ do -- AI_A2A_DISPATCHER self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } ) self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } ) - -- DefenderSquadron.Resources can have the value nil, which expresses unlimited resources. - -- DefendersNeeded cannot exceed DefenderSquadron.Resources! - if DefenderSquadron.Resources and DefendersNeeded > DefenderSquadron.Resources then - DefendersNeeded = DefenderSquadron.Resources + -- DefenderSquadron.ResourceCount can have the value nil, which expresses unlimited resources. + -- DefendersNeeded cannot exceed DefenderSquadron.ResourceCount! + if DefenderSquadron.ResourceCount and DefendersNeeded > DefenderSquadron.ResourceCount then + DefendersNeeded = DefenderSquadron.ResourceCount BreakLoop = true end while ( DefendersNeeded > 0 ) do - local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN - local DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded - if DefenderGrouping then - Spawn:InitGrouping( DefenderGrouping ) - else - Spawn:InitGrouping() - end - - local TakeoffMethod = self:GetSquadronTakeoff( ClosestDefenderSquadronName ) - local DefenderGCI = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP - self:F( { GCIDefender = DefenderGCI:GetName() } ) + local DefenderGCI, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) DefendersNeeded = DefendersNeeded - DefenderGrouping - self:AddDefenderToSquadron( DefenderSquadron, DefenderGCI, DefenderGrouping ) - if DefenderGCI then DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead @@ -2919,6 +3060,7 @@ do -- AI_A2A_DISPATCHER if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) Defender:Destroy() + self:ParkDefender( Squadron, Defender ) end end end -- if DefenderGCI then @@ -3500,7 +3642,7 @@ do -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. - -- @param #number Resources The amount of resources that will be allocated to each squadron. + -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. -- @return #AI_A2A_GCICAP -- @usage -- @@ -3575,7 +3717,7 @@ do -- -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, nil, nil, nil, nil, nil, 30 ) -- - function AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, Resources ) + function AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) local EWRSetGroup = SET_GROUP:New() EWRSetGroup:FilterPrefixes( EWRPrefixes ) @@ -3629,7 +3771,7 @@ do end end if Templates then - self:SetSquadron( AirbaseName, AirbaseName, Templates, Resources ) + self:SetSquadron( AirbaseName, AirbaseName, Templates, ResourceCount ) end end @@ -3706,7 +3848,7 @@ do -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. - -- @param #number Resources The amount of resources that will be allocated to each squadron. + -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. -- @return #AI_A2A_GCICAP -- @usage -- @@ -3790,9 +3932,9 @@ do -- -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", nil, nil, nil, nil, nil, 30 ) -- - function AI_A2A_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, Resources ) + function AI_A2A_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) - local self = AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, Resources ) + local self = AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) if BorderPrefix then self:SetBorderZone( ZONE_POLYGON:New( BorderPrefix, GROUP:FindByName( BorderPrefix ) ) ) diff --git a/Moose Development/Moose/AI/AI_A2G.lua b/Moose Development/Moose/AI/AI_A2G.lua new file mode 100644 index 000000000..2f6a8f500 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G.lua @@ -0,0 +1,69 @@ +--- **AI** -- Models the process of air to ground operations for airplanes and helicopters. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G +-- @image AI_Air_To_Ground_Dispatching.JPG + +--- @type AI_A2G +-- @extends AI.AI_Air#AI_AIR + +--- The AI_A2G class implements the core functions to operate an AI @{Wrapper.Group} A2G tasking. +-- +-- +-- # 1) AI_A2G constructor +-- +-- * @{#AI_A2G.New}(): Creates a new AI_A2G object. +-- +-- # 2) AI_A2G is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- +-- ## 2.1) AI_A2G States. +-- +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_A2G Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- @field #AI_A2G +AI_A2G = { + ClassName = "AI_A2G", +} + +--- Creates a new AI_A2G process. +-- @param #AI_A2G self +-- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process. +-- @return #AI_A2G +function AI_A2G:New( AIGroup ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_AIR:New( AIGroup ) ) -- #AI_A2G + + self:SetFuelThreshold( .2, 60 ) + self:SetDamageThreshold( 0.4 ) + self:SetDisengageRadius( 70000 ) + + return self +end + diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua new file mode 100644 index 000000000..fde91d028 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -0,0 +1,4146 @@ +--- **AI** - Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. +-- +-- === +-- +-- Features: +-- +-- * Setup quickly an A2G defense system for a coalition. +-- * Setup multiple defense zones to defend specif points in your battlefield. +-- * Setup (SEAD) suppression of air defenses to enhance the control of enemy airspace. +-- * Setup (CAS) Controlled Air Support to attack approach enemy ground units. +-- * Setup (BAI) Battleground Air Interdiction to attack detected remote enemy ground units and targets. +-- * Define and use a detection network setup by recce. +-- * Define defense squadrons at airbases, farps and carriers. +-- * Enable airbases for A2G defenses. +-- * Add different planes and helicopter templates to different squadrons. +-- * Assign squadrons to execute a specific engagement type depending on threat level of the detected ground enemy unit composition. +-- * Add multiple squadrons to different airbases, farps or carriers. +-- * Define different ranges to engage upon. +-- * Establish an automatic in air refuel process for planes using refuel tankers. +-- * Setup default settings for all squadrons and A2G defenses. +-- * Setup specific settings for specific squadrons. +-- +-- === +-- +-- ## Missions: +-- +-- [AID-A2G - AI A2G Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2G%20-%20AI%20A2G%20Dispatching) +-- +-- === +-- +-- ## YouTube Channel: +-- +-- [DCS WORLD - MOOSE - A2G GCICAP - Build an automatic A2G Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) +-- +-- === +-- +-- # QUICK START GUIDE +-- +-- The following class is available to model an A2G defense system. +-- +-- AI_A2G_DISPATCHER is the main A2G defense class that models the A2G defense system. +-- +-- Before you start using the AI_A2G_DISPATCHER, ask youself the following questions. +-- +-- +-- ## 1. Which coalition am I modeling an A2G defense system for? blue or red? +-- +-- One AI_A2G_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. +-- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI_A2G_DISPATCHER **objects**, +-- each governing their defense system for one coalition. +-- +-- +-- ## 2. Which type of detection will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). +-- +-- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units and reporting them to the head quarters. +-- Several types of @{Functional.Detection} classes exist, and the most common characteristics of these classes is that they: +-- +-- * Perform detections from multiple recce as one co-operating entity. +-- * Communicate with a @{Tasking.CommandCenter}, which consolidates each detection. +-- * Groups detections based on a method (per area, per type or per unit). +-- * Communicates detections. +-- +-- +-- ## 3. Which recce units can be used as part of the detection system? Only Ground or also Airborne? +-- +-- Depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets. +-- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range. +-- Recce are very useful to acquire the position of enemy ground targets when spread out over the battlefield at strategic positions. +-- Ground units also have varying detectors, and especially the ground units which have laser guiding missiles can be extremely effective at +-- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. +-- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then +-- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! +-- +-- Beside ground level units to use for reconnaissance, air units are also very effective. The are capable of patrolling at great speed +-- covering a large terrain. However, airborne recce can be vulnerable to air to ground attacks, and you need air superiority to make then +-- effective. Also the instruments available at the air units play a big role in the effectiveness of the reconnaissance. +-- Air units which have ground detection capabilities will be much more effective than air units with only visual detection capabilities. +-- For the red coalition, the Mi-28N and for the blue side, the reaper are such effective reconnaissance airborne units. +-- +-- +-- ## 4. How do defenses decide to engage on approaching enemy units? +-- +-- The A2G dispacher needs you to setup defense coordinates, which are specific coordinates that are strategic positions in the battle field +-- to be defended. Any ground based enemy approaching to such a defense point, will be engaged for defense by A2G defense units. +-- The A2G dispatcher provides parameters to setup the defensiveness, meaning, when actually A2G units will engage with the approaching enemy. +-- For this, a probability distribution model has been created, which models an increased probability that a defense will engage an attacker, +-- depending on the distance of the attacker to the defense coordinate. There are 3 levels of defense reactivity setup, which are Low, Medium and High. +-- Defenses will start to consider defensive action when an enemy ground unit is within 60km from a defense point, by default. +-- But you can change this maximum distance using on of the available methods. The close the attacker is to the defense point, the +-- higher the probability will be that a defense action will be launched! +-- +-- +-- ## 5. Are defense coordinates and defense reactivity the only parameters? +-- +-- No, depending on the target type, and the threat level of the target, the probability of defense will be higher. +-- In other words, when a SAM-10 radar emitter is detected, its probabilty for defense will be much higher than when a BMP-1 vehicle is +-- detected, even when both are at the same distance from a defense coordinate. +-- This will ensure optimal defenses, SEAD tasks will be much more quicker launched agains radar emitters, to ensure air superiority. +-- Approaching main battle tanks will be much faster defended upon, than a group of approaching trucks. +-- +-- +-- ## 6. Which Squadrons will I create and which name will I give each Squadron? +-- +-- The A2G defense system works with **Squadrons**. Each Squadron must be given a unique name, that forms the **key** to the squadron. +-- Several options and activities can be set per Squadron. +-- +-- There are mainly 3 types of defenses: SEAD, CAS and BAI. +-- +-- Suppression of Air Defenses (SEAD) are effective agains radar emitters. Close Air Support (CAS) is launched when the enemy is close near friendly units. +-- Battleground Air Interdiction (BAI) tasks are launched when there are no friendlies around. +-- +-- Depending on the defense type, different payloads will be needed. See further points on squadron definition. +-- +-- ## 7. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? +-- +-- Squadrons are placed as the "home base" on an airfield, carrier or farp. +-- Carefully plan where each Squadron will be located as part of the defense system. +-- Any airbase, farp or carrier can act as the launching platform for A2G defenses. +-- Carefully plan which airbases will take part in the coalition. Color each airbase in the color of the coalition. +-- +-- +-- ## 8. Which helicopter or plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron? +-- +-- Per Squadron, one or multiple helicopter or plane models can be allocated as **Templates**. +-- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**. +-- The A2G defense system will select from the given templates a random template to spawn a new plane (group). +-- +-- A squadron will perform specific task types (SEAD, CAS or BAI). So, squadrons will require specific templates for the +-- task types it will perform. A squadron executing SEAD defenses, will require a payload with long range anti-radar seeking missiles. +-- +-- +-- ## 9. Which payloads, skills and skins will these plane models have? +-- +-- Per Squadron, even if you have one plane model, you can still allocate multiple templates of one plane model, +-- each having different payloads, skills and skins. +-- The A2G defense system will select from the given templates a random template to spawn a new plane (group). +-- +-- +-- ## 10. How to squadrons engage in a defensive action? +-- +-- There are two ways how squadrons engage and execute your A2G defenses. +-- Squadrons can start the defense directly from the airbase, farp or carrier. When a squadron launches a defensive group, that group +-- will start directly from the airbase. The other way is to launch early on in the mission a patrolling mechanism. +-- Squadrons will launch air units to patrol in specific zone(s), so that when ground enemy targets are detected, that the airborne +-- A2G defenses can come immediately into action. +-- +-- +-- ## 11. For each Squadron doing a patrol, which zone types will I create? +-- +-- Per zone, evaluate whether you want: +-- +-- * simple trigger zones +-- * polygon zones +-- * moving zones +-- +-- Depending on the type of zone selected, a different @{Zone} object needs to be created from a ZONE_ class. +-- +-- +-- ## 12. Are moving defense coordinates possible? +-- +-- Yes, different COORDINATE types are possible to be used. +-- The COORDINATE_UNIT will help you to specify a defense coodinate that is attached to a moving unit. +-- +-- +-- ## 13. How much defense coordinates do I need to create? +-- +-- It depends, but the idea is to define only the necessary defense points that drive your mission. +-- If you define too much defense points, the performance of your mission may decrease. Per defense point defined, +-- all the possible enemies are evaluated. Note that each defense coordinate has a reach depending on the size of the defense radius. +-- The default defense radius is about 60km, and depending on the defense reactivity, defenses will be launched when the enemy is at +-- close or greater distance from the defense coordinate. +-- +-- +-- ## 14. For each Squadron doing patrols, what are the time intervals and patrol amounts to be performed? +-- +-- For each patrol: +-- +-- * **How many** patrol you want to have airborne at the same time? +-- * **How frequent** you want the defense mechanism to check whether to start a new patrol? +-- +-- other considerations: +-- +-- * **How far** is the patrol area from the engagement "hot zone". You want to ensure that the enemy is reached on time! +-- * **How safe** is the patrol area taking into account air superiority. Is it well defended, are there nearby A2A bases? +-- +-- +-- ## 15. For each Squadron, which takeoff method will I use? +-- +-- For each Squadron, evaluate which takeoff method will be used: +-- +-- * Straight from the air +-- * From the runway +-- * From a parking spot with running engines +-- * From a parking spot with cold engines +-- +-- **The default takeoff method is staight in the air.** +-- This takeoff method is the most useful if you want to avoid airplane clutter at airbases! +-- But it is the least realistic one! +-- +-- +-- ## 16. For each Squadron, which landing method will I use? +-- +-- For each Squadron, evaluate which landing method will be used: +-- +-- * Despawn near the airbase when returning +-- * Despawn after landing on the runway +-- * Despawn after engine shutdown after landing +-- +-- **The default landing method is despawn when near the airbase when returning.** +-- This landing method is the most useful if you want to avoid airplane clutter at airbases! +-- But it is the least realistic one! +-- +-- +-- ## 19. For each Squadron, which **defense overhead** will I use? +-- +-- For each Squadron, depending on the helicopter or airplane type (modern, old) and payload, which overhead is required to provide any defense? +-- +-- In other words, if **X** enemy ground units are detected, how many **Y** defense helicpters or airplanes need to engage (per squadron)? +-- The **Y** is dependent on the type of airplane (era), payload, fuel levels, skills etc. +-- But the most important factor is the payload, which is the amount of A2G weapons the defense can carry to attack the enemy ground units. +-- For example, a Ka-50 can carry 16 vikrs, that means, that it potentially can destroy at least 8 ground units without a reload of ammunication. +-- That means, that one defender can destroy more enemy ground units. +-- Thus, the overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected. +-- +-- **The default overhead is 1. A smaller value than 1, like 0.25 will decrease the overhead to a 1 / 4 ratio, meaning, +-- one defender for each 4 detected ground enemy units. ** +-- +-- +-- ## 19. For each Squadron, which grouping will I use? +-- +-- When multiple targets are detected, how will defenses be grouped when multiple defense air units are spawned for multiple enemy ground units? +-- Per one, two, three, four? +-- +-- **The default grouping is 1. That means, that each spawned defender will act individually.** +-- But you can specify a number between 1 and 4, so that the defenders will act as a group. +-- +-- === +-- +-- ### Author: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons). +-- +-- @module AI.AI_A2G_Dispatcher +-- @image AI_Air_To_Ground_Dispatching.JPG + + + +do -- AI_A2G_DISPATCHER + + --- AI_A2G_DISPATCHER class. + -- @type AI_A2G_DISPATCHER + -- @extends Tasking.DetectionManager#DETECTION_MANAGER + + --- Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. + -- + -- === + -- + -- When your mission is in the need to take control of the AI to automate and setup a process of air to ground defenses, this is the module you need. + -- The defense system work through the definition of defense coordinates, which are points in your friendly area within the battle field, that your mission need to have defended. + -- Multiple defense coordinates can be setup. Defense coordinates can be strategic or tactical positions or references to strategic units or scenery. + -- The A2G dispatcher will evaluate every x seconds the tactical situation around each defense coordinate. When a defense coordinate + -- is under threat, it will communicate through the command center that defensive actions need to be taken and will launch groups of air units for defense. + -- The level of threat to the defense coordinate varyies upon the strength and types of the enemy units, the distance to the defense point, and the defensiveness parameters. + -- Defensive actions are taken through probability, but the closer and the more threat the enemy poses to the defense coordinate, the faster it will be attacked by friendly A2G units. + -- + -- Please study carefully the underlying explanations how to setup and use this module, as it has many features. + -- It also requires a little study to ensure that you get a good understanding of the defense mechanisms, to ensure a strong + -- defense for your missions. + -- + -- === + -- + -- # USAGE GUIDE + -- + -- ## 1. AI\_A2G\_DISPATCHER constructor: + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_DISPATCHER-ME_1.JPG) + -- + -- + -- The @{#AI_A2G_DISPATCHER.New}() method creates a new AI_A2G_DISPATCHER instance. + -- + -- ### 1.1. Define the **reconnaissance network**: + -- + -- As part of the AI_A2G_DISPATCHER :New() constructor, a reconnaissance network must be given as the first parameter. + -- A reconnaissance network is provide through an instance of a @{Functional.Detection} network. + -- The most effective reconnaissance for the A2G dispatcher would be to use the @{Functional.Detection#DETECTION_AREAS} object. + -- + -- An reconnaissance network, is used to detect enemy ground targets, potentially group them into areas, and to understand the position, level of threat of the enemy. + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia5.JPG) + -- + -- As explained in the introduction, depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets. + -- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range. + -- Recce are very useful to acquire the position of enemy ground targets when spread out over the battlefield at strategic positions. + -- Ground units also have varying detectors, and especially the ground units which have laser guiding missiles can be extremely effective at + -- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. + -- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then + -- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! + -- + -- Beside ground level units to use for reconnaissance, air units are also very effective. The are capable of patrolling at great speed + -- covering a large terrain. However, airborne recce can be vulnerable to air to ground attacks, and you need air superiority to make then + -- effective. Also the instruments available at the air units play a big role in the effectiveness of the reconnaissance. + -- Air units which have ground detection capabilities will be much more effective than air units with only visual detection capabilities. + -- For the red coalition, the Mi-28N and for the blue side, the reaper are such effective reconnaissance airborne units. + -- + -- Reconnaissance networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection} instance that is given as the first parameter to the A2G dispatcher. + -- By defining in a **smart way the names or name prefixes of the reconnaissance groups**, these groups will be **automatically added or removed** to or from the reconnaissance network, + -- when these groups are spawned in or destroyed during the ongoing battle. + -- By spawning in dynamically additional recce, you can ensure that there is sufficient reconnaissance coverage so the defense mechanism is continuously + -- alerted of new enemy ground targets. + -- + -- The following example defens a new reconnaissance network using a @{Functional.Detection#DETECTION_AREAS} object. + -- + -- -- Define a SET_GROUP object that builds a collection of groups that define the recce network. + -- -- Here we build the network with all the groups that have a name starting with CCCP Recce. + -- DetectionSetGroup = SET_GROUP:New() -- Defene a set of group objects, caled DetectionSetGroup. + -- + -- DetectionSetGroup:FilterPrefixes( { "CCCP Recce" } ) -- The DetectionSetGroup will search for groups that start with the name "CCCP Recce". + -- + -- -- This command will start the dynamic filtering, so when groups spawn in or are destroyed, + -- -- which have a group name starting with "CCCP Recce", then these will be automatically added or removed from the set. + -- DetectionSetGroup:FilterStart() + -- + -- -- This command defines the reconnaissance network. + -- -- It will group any detected ground enemy targets within a radius of 1km. + -- -- It uses the DetectionSetGroup, which defines the set of reconnaissance groups to detect for enemy ground targets. + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 1000 ) + -- + -- -- Setup the A2A dispatcher, and initialize it. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- + -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **DetectionSetGroup**. + -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with `"CCCP Recce"` to be included in the set. + -- **DetectionSetGroup** is then calling `FilterStart()`, which is starting the dynamic filtering or inclusion of these groups. + -- Note that any destroy or new spawn of a group having a name, starting with the above prefix, will be removed or added to the set. + -- + -- Then a new detection object is created from the class `DETECTION_AREAS`. A grouping radius of 1000 meters (1km) is choosen. + -- + -- The `Detection` object is then passed to the @{#AI_A2G_DISPATCHER.New}() method to indicate the reconnaissance network + -- configuration and setup the A2G defense detection mechanism. + -- + -- ### 1.2. Setup the A2G dispatcher for both a red and blue coalition. + -- + -- Following the above described procedure, you'll need to create for each coalition an separate detection network, and a separate A2G dispatcher. + -- Ensure that while doing so, that you name the objects differently both for red and blue coalition. + -- + -- For example like this for the red coalition: + -- + -- DetectionRed = DETECTION_AREAS:New( DetectionSetGroupRed, 1000 ) + -- A2GDispatcherRed = AI_A2G_DISPATCHER:New( DetectionRed ) + -- + -- And for the blue coalition: + -- + -- DetectionBlue = DETECTION_AREAS:New( DetectionSetGroupBlue, 1000 ) + -- A2GDispatcherBlue = AI_A2G_DISPATCHER:New( DetectionBlue ) + -- + -- + -- Note: Also the SET_GROUP objects should be created for each coalition separately, containing each red and blue recce respectively! + -- + -- ### 1.3. Define the enemy ground target **grouping radius**, in case you use DETECTION_AREAS: + -- + -- The target grouping radius is a property of the DETECTION_AREAS class, that was passed to the AI_A2G_DISPATCHER:New() method, + -- but can be changed. The grouping radius should not be too small, but also depends on the types of ground forces and the way you want your mission to evolve. + -- A large radius will mean large groups of enemy ground targets, while making smaller groups will result in a more fragmented defense system. + -- Typically I suggest a grouping radius of 1km. This is the right balance to create efficient defenses. + -- + -- Note that detected targets are constantly re-grouped, that is, when certain detected enemy ground units are moving further than the group radius, + -- then these units will become a separate area being detected. This may result in additional defenses being started by the dispatcher! + -- So don't make this value too small! Again, I advise about 1km or 1000 meters. + -- + -- ## 2. Setup (a) **Defense Coordinate(s)**. + -- + -- As explained above, defense coordinates are the center of your defense operations. + -- The more threat to the defense coordinate, the higher it is likely a defensive action will be launched. + -- + -- Find below an example how to add defense coordinates: + -- + -- -- Add defense coordinates. + -- A2GDispatcher:AddDefenseCoordinate( "HQ", GROUP:FindByName( "HQ" ):GetCoordinate() ) + -- + -- In this example, the coordinate of a group called `"HQ"` is retrieved, using `:GetCoordinate()` + -- This returns a COORDINATE object, pointing to the first unit within the GROUP object. + -- + -- The method @{#AI_A2G_DISPATCHER.AddDefenseCoordinate}() adds a new defense coordinate to the `A2GDispatcher` object. + -- The first parameter is the key of the defense coordinate, the second the coordinate itself. + -- + -- Later, a COORDINATE_UNIT will be added to the framework, which can be used to assign "moving" coordinates to an A2G dispatcher. + -- + -- **REMEMBER!** + -- + -- - **Defense coordinates are the center of the A2G dispatcher defense system!** + -- - **You can define more defense coordinates to defend a larger area.** + -- - **Detected enemy ground targets are not immediately engaged, but are engaged with a reactivity or probability calculation!** + -- + -- But, there is more to it ... + -- + -- + -- ### 2.1. The **Defense Radius**. + -- + -- The defense radius defines the maximum radius that a defense will be initiated around each defense coordinate. + -- So even when there are targets further away than the defense radius, then these targets won't be engaged upon. + -- By default, the defense radius is set to 100km (100.000 meters), but can be changed using the @{#AI_A2G_DISPATCHER.SetDefenseRadius}() method. + -- Note that the defense radius influences the defense reactivity also! The larger the defense radius, the more reactive the defenses will be. + -- + -- For example: + -- + -- A2GDispatcher:SetDefenseRadius( 30000 ) + -- + -- This defines an A2G dispatcher which will engage on enemy ground targets within 30km radius around the defense coordinate. + -- Note that the defense radius **applies to all defense coordinates** defined within the A2G dispatcher. + -- + -- ### 2.2. The **Defense Reactivity**. + -- + -- There are 5 levels that can be configured to tweak the defense reactivity. As explained above, the threat to a defense coordinate is + -- also determined by the distance of the enemy ground target to the defense coordinate. + -- If you want to have a **low** defense reactivity, that is, the probability that an A2G defense will engage to the enemy ground target, then + -- use the @{#AI_A2G_DISPATCHER.SetDefenseReactivityLow}() method. For medium and high reactivity, use the methods + -- @{#AI_A2G_DISPATCHER.SetDefenseReactivityMedium}() and @{#AI_A2G_DISPATCHER.SetDefenseReactivityHigh}() respectively. + -- + -- Note that the reactivity of defenses is always in relation to the Defense Radius! the shorter the distance, + -- the less reactive the defenses will be in terms of distance to enemy ground targets! + -- + -- For example: + -- + -- A2GDispatcher:SetDefenseReactivityHigh() + -- + -- This defines an A2G dispatcher with high defense reactivity. + -- + -- ## 3. **Squadrons**. + -- + -- The A2G dispatcher works with **Squadrons**, that need to be defined using the different methods available. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, farp or carrier, + -- while defining which helicopter or plane **templates** are being used by the squadron and how many **resources** are available. + -- + -- **Multiple squadrons** can be defined within one A2G dispatcher, each having specific defense tasks and defense parameter settings! + -- + -- Squadrons: + -- + -- * Have name (string) that is the identifier or **key** of the squadron. + -- * Have specific helicopter or plane **templates**. + -- * Are located at **one** airbase, farp or carrier. + -- * Optionally have a **limited set of resources**. The default is that squadrons have **unlimited resources**. + -- + -- The name of the squadron given acts as the **squadron key** in all `A2GDispatcher:SetSquadron...()` or `A2GDispatcher:GetSquadron...()` methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new helicopters or aircraft are taking off from the airfield, farp or carrier (in the air, cold, hot, at the runway). + -- * Control how returning helicopters or aircraft are landing at the airfield, farp or carrier (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new helicopters or aircraft spawned at the airfield, farp or carrier. If there is more than one helicopter or aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of helicopters, planes, amount of resources and payload (weapon configuration) chosen, + -- the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- The method @{#AI_A2G_DISPATCHER.SetSquadron}() defines for you a new squadron. + -- The provided parameters are the squadron name, airbase name and a list of template prefixe, and a number that indicates the amount of resources. + -- + -- For example, this defines 3 new squadrons: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50" }, 10 ) + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50" }, 10 ) + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50" }, 10 ) + -- + -- The latter 2 will depart from FARPs, which bare the name `"CAS"` and `"BAI"`. + -- + -- + -- ### 3.1. Squadrons **Tasking**. + -- + -- Squadrons can be commanded to execute 3 types of tasks, as explained above: + -- + -- - SEAD: Suppression of Air Defenses, which are ground targets that have medium or long range radar emitters. + -- - CAS : Close Air Support, when there are enemy ground targets close to friendly units. + -- - BAI : Battlefield Air Interdiction, which are targets further away from the frond-line. + -- + -- You need to configure each squadron which task types you want it to perform. Read on ... + -- + -- ### 3.2. Squadrons enemy ground target **Engagement**. + -- + -- There are two ways how targets can be engaged: directly upon call from the airfield, farp or carrier, or through a patrol. + -- + -- Patrols are extremely handy, as these will airborne your helicopters or airplanes in advance. They will patrol in defined zones outlined, + -- and will engage with the targets once commanded. If the patrol zone is close enough to the enemy ground targets, then the time required + -- to engage is heavily minimized! + -- + -- However; patrols come with a side effect: since your resources are airborne, they will be vulnerable to incoming air attacks from the enemy. + -- + -- The mission designer needs to carefully balance the need for patrols or the need for engagement on call from the airfields. + -- + -- ### 3.3. Squadron **on call engagement**. + -- + -- So to make squadrons engage targets from the airfields, use the following methods: + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSead}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCas}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBai}() method. + -- + -- Note that for the tasks, specific helicopter or airplane templates are required to be used, which you can configure using your mission editor. + -- Especially the payload (weapons configuration) is important to get right. + -- + -- For example, the following will define for the squadrons different tasks: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- A2GDispatcher:SetSquadronSead( "Maykop SEAD", 120, 250 ) + -- + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- A2GDispatcher:SetSquadronCas( "Maykop CAS", 120, 250 ) + -- + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- A2GDispatcher:SetSquadronBai( "Maykop BAI", 120, 250 ) + -- + -- ### 3.4. Squadron **on patrol engagement**. + -- + -- Squadrons can be setup to patrol in the air near the engagement hot zone. + -- When needed, the A2G defense units will be close to the battle area, and can engage quickly. + -- + -- So to make squadrons engage targets from a patrol zone, use the following methods: + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrol}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrol}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrol}() method. + -- + -- Because a patrol requires more parameters, the following methods must be used to fine-tune the patrols for each squadron. + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrolInterval}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrolInterval}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrolInterval}() method. + -- + -- Here an example to setup patrols of various task types: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Maykop SEAD", PatrolZone, 300, 500, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop SEAD", 2, 30, 60, 1, "SEAD" ) + -- + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- A2GDispatcher:SetSquadronCasPatrol( "Maykop CAS", PatrolZone, 600, 700, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop CAS", 2, 30, 60, 1, "CAS" ) + -- + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Maykop BAI", PatrolZone, 800, 900, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop BAI", 2, 30, 60, 1, "BAI" ) + -- + -- @field #AI_A2G_DISPATCHER + AI_A2G_DISPATCHER = { + ClassName = "AI_A2G_DISPATCHER", + Detection = nil, + } + + + --- List of defense coordinates. + -- @type AI_A2G_DISPATCHER.DefenseCoordinates + -- @map <#string,Core.Point#COORDINATE> A list of all defense coordinates mapped per defense coordinate name. + + --- @field #AI_A2G_DISPATCHER.DefenseCoordinates DefenseCoordinates + AI_A2G_DISPATCHER.DefenseCoordinates = {} + + --- Enumerator for spawns at airbases + -- @type AI_A2G_DISPATCHER.Takeoff + -- @extends Wrapper.Group#GROUP.Takeoff + + --- @field #AI_A2G_DISPATCHER.Takeoff Takeoff + AI_A2G_DISPATCHER.Takeoff = GROUP.Takeoff + + --- Defnes Landing location. + -- @field Landing + AI_A2G_DISPATCHER.Landing = { + NearAirbase = 1, + AtRunway = 2, + AtEngineShutdown = 3, + } + + --- AI_A2G_DISPATCHER constructor. + -- This is defining the A2G DISPATCHER for one coaliton. + -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units. + -- The Detection object is polymorphic, depending on the type of detection object choosen, the detection will work differently. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. + -- @return #AI_A2G_DISPATCHER self + -- @usage + -- + -- -- Setup the Detection, using DETECTION_AREAS. + -- -- First define the SET of GROUPs that are defining the EWR network. + -- -- Here with prefixes DF CCCP AWACS, DF CCCP EWR. + -- DetectionSetGroup = SET_GROUP:New() + -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) + -- DetectionSetGroup:FilterStart() + -- + -- -- Define the DETECTION_AREAS, using the DetectionSetGroup, with a 30km grouping radius. + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) -- + -- + function AI_A2G_DISPATCHER:New( Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( nil, Detection ) ) -- #AI_A2G_DISPATCHER + + self.Detection = Detection -- Functional.Detection#DETECTION_AREAS + + self.Detection:FilterCategories( Unit.Category.GROUND_UNIT ) + + -- This table models the DefenderSquadron templates. + self.DefenderSquadrons = {} -- The Defender Squadrons. + self.DefenderSpawns = {} + self.DefenderTasks = {} -- The Defenders Tasks. + self.DefenderDefault = {} -- The Defender Default Settings over all Squadrons. + + -- TODO: Check detection through radar. +-- self.Detection:FilterCategories( { Unit.Category.GROUND } ) +-- self.Detection:InitDetectRadar( false ) +-- self.Detection:InitDetectVisual( true ) +-- self.Detection:SetRefreshTimeInterval( 30 ) + + self:SetDefenseRadius() + self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. + self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Air ) + self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.NearAirbase ) + self:SetDefaultOverhead( 1 ) + self:SetDefaultGrouping( 1 ) + self:SetDefaultFuelThreshold( 0.15, 0 ) -- 15% of fuel remaining in the tank will trigger the airplane to return to base or refuel. + self:SetDefaultDamageThreshold( 0.4 ) -- When 40% of damage, go RTB. + self:SetDefaultPatrolTimeInterval( 180, 600 ) -- Between 180 and 600 seconds. + self:SetDefaultPatrolLimit( 1 ) -- Maximum one Patrol per squadron. + + + self:AddTransition( "Started", "Assign", "Started" ) + + --- OnAfter Transition Handler for Event Assign. + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterAssign + -- @param #AI_A2G_DISPATCHER self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Tasking.Task_A2G#AI_A2G Task + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #string PlayerName + + self:AddTransition( "*", "Patrol", "*" ) + + --- Patrol Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforePatrol + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Patrol Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterPatrol + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Patrol Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Patrol + -- @param #AI_A2G_DISPATCHER self + + --- Patrol Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Patrol + -- @param #AI_A2G_DISPATCHER self + -- @param #number Delay + + self:AddTransition( "*", "Defend", "*" ) + + --- Defend Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeDefend + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Defend Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterDefend + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Defend Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Defend + -- @param #AI_A2G_DISPATCHER self + + --- Defend Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Defend + -- @param #AI_A2G_DISPATCHER self + -- @param #number Delay + + self:AddTransition( "*", "Engage", "*" ) + + --- Engage Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeEngage + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Engage Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterEngage + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Engage Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Engage + -- @param #AI_A2G_DISPATCHER self + + --- Engage Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Engage + -- @param #AI_A2G_DISPATCHER self + -- @param #number Delay + + + -- Subscribe to the CRASH event so that when planes are shot + -- by a Unit from the dispatcher, they will be removed from the detection... + -- This will avoid the detection to still "know" the shot unit until the next detection. + -- Otherwise, a new defense or engage may happen for an already shot plane! + + + self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) + self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) + + + self:HandleEvent( EVENTS.Land ) + self:HandleEvent( EVENTS.EngineShutdown ) + + -- Handle the situation where the airbases are captured. + self:HandleEvent( EVENTS.BaseCaptured ) + + self:SetTacticalDisplay( false ) + + self.DefenderPatrolIndex = 0 + + self:SetDefenseReactivityMedium() + + self:__Start( 5 ) + + return self + end + + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterStart( From, Event, To ) + + self:GetParent( self ).onafterStart( self, From, Event, To ) + + -- Spawn the resources. + for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + DefenderSquadron.Resource = {} + for Resource = 1, DefenderSquadron.ResourceCount or 0 do + self:ParkDefender( DefenderSquadron ) + end + end + end + + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ParkDefender( DefenderSquadron ) + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) + local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN + Spawn:InitGrouping( 1 ) + local SpawnGroup + if self:IsSquadronVisible( DefenderSquadron.Name ) then + SpawnGroup = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, SPAWN.Takeoff.Cold ) + local GroupName = SpawnGroup:GetName() + DefenderSquadron.Resources = DefenderSquadron.Resources or {} + DefenderSquadron.Resources[TemplateID] = DefenderSquadron.Resources[TemplateID] or {} + DefenderSquadron.Resources[TemplateID][GroupName] = {} + DefenderSquadron.Resources[TemplateID][GroupName] = SpawnGroup + end + end + + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventBaseCaptured( EventData ) + + local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured. + + self:I( "Captured " .. AirbaseName ) + + -- Now search for all squadrons located at the airbase, and sanatize them. + for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do + if Squadron.AirbaseName == AirbaseName then + Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. + Squadron.Captured = true + self:I( "Squadron " .. SquadronName .. " captured." ) + end + end + end + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventCrashOrDead( EventData ) + self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) + end + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventLand( EventData ) + self:F( "Landed" ) + local DefenderUnit = EventData.IniUnit + local Defender = EventData.IniGroup + local Squadron = self:GetSquadronFromDefender( Defender ) + if Squadron then + self:F( { SquadronName = Squadron.Name } ) + local LandingMethod = self:GetSquadronLanding( Squadron.Name ) + if LandingMethod == AI_A2G_DISPATCHER.Landing.AtRunway then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ParkDefender( Squadron, Defender ) + return + end + if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then + -- Damaged units cannot be repaired anymore. + DefenderUnit:Destroy() + return + end + end + end + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventEngineShutdown( EventData ) + local DefenderUnit = EventData.IniUnit + local Defender = EventData.IniGroup + local Squadron = self:GetSquadronFromDefender( Defender ) + if Squadron then + self:F( { SquadronName = Squadron.Name } ) + local LandingMethod = self:GetSquadronLanding( Squadron.Name ) + if LandingMethod == AI_A2G_DISPATCHER.Landing.AtEngineShutdown and + not DefenderUnit:InAir() then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ParkDefender( Squadron, Defender ) + end + end + end + + do -- Manage the defensive behaviour + + --- @param #AI_A2G_DISPATCHER self + -- @param #string DefenseCoordinateName The name of the coordinate to be defended by A2G defenses. + -- @param Core.Point#COORDINATE DefenseCoordinate The coordinate to be defended by A2G defenses. + function AI_A2G_DISPATCHER:AddDefenseCoordinate( DefenseCoordinateName, DefenseCoordinate ) + self.DefenseCoordinates[DefenseCoordinateName] = DefenseCoordinate + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenseReactivityLow() + self.DefenseReactivity = 0.05 + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenseReactivityMedium() + self.DefenseReactivity = 0.15 + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenseReactivityHigh() + self.DefenseReactivity = 0.5 + end + + end + + --- Define the radius to engage any target by airborne friendlies, which are executing cap or returning from an defense mission. + -- If there is a target area detected and reported, then any friendlies that are airborne near this target area, + -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). + -- + -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, + -- will be considered to receive the command to engage that target area. + -- + -- You need to evaluate the value of this parameter carefully: + -- + -- * If too small, more defense missions may be triggered upon detected target areas. + -- * If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. + -- + -- **Use the method @{#AI_A2G_DISPATCHER.SetEngageRadius}() to modify the default Engage Radius for ALL squadrons.** + -- + -- Demonstration Mission: [AID-019 - AI_A2G - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2G%20-%20Engage%20Range%20Test) + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Set 50km as the radius to engage any target by airborne friendlies. + -- A2GDispatcher:SetEngageRadius( 50000 ) + -- + -- -- Set 100km as the radius to engage any target by airborne friendlies. + -- A2GDispatcher:SetEngageRadius() -- 100000 is the default value. + -- + function AI_A2G_DISPATCHER:SetEngageRadius( EngageRadius ) + + --self.Detection:SetFriendliesRange( EngageRadius or 100000 ) + + return self + end + + --- Define the radius to disengage any target when the distance to the home base is larger than the specified meters. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DisengageRadius (Optional, Default = 300000) The radius to disengage a target when too far from the home base. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Set 50km as the Disengage Radius. + -- A2GDispatcher:SetDisengageRadius( 50000 ) + -- + -- -- Set 100km as the Disengage Radius. + -- A2GDispatcher:SetDisngageRadius() -- 300000 is the default value. + -- + function AI_A2G_DISPATCHER:SetDisengageRadius( DisengageRadius ) + + self.DisengageRadius = DisengageRadius or 300000 + + return self + end + + + --- Define the defense radius to check if a target can be engaged by a squadron group for SEAD, CAS or BAI for defense. + -- When targets are detected that are still really far off, you don't want the AI_A2G_DISPATCHER to launch defenders, as they might need to travel too far. + -- You want it to wait until a certain defend radius is reached, which is calculated as: + -- 1. the **distance of the closest airbase to target**, being smaller than the **Defend Radius**. + -- 2. the **distance to any defense reference point**. + -- + -- The **default** defense radius is defined as **400000** or **40km**. Override the default defense radius when the era of the warfare is early, or, + -- when you don't want to let the AI_A2G_DISPATCHER react immediately when a certain border or area is not being crossed. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefendRadius}() to set a specific defend radius for all squadrons, + -- **the Defense Radius is defined for ALL squadrons which are operational.** + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #number DefenseRadius (Optional, Default = 200000) The defense radius to engage detected targets from the nearest capable and available squadron airbase. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Set 100km as the radius to defend from detected targets from the nearest airbase. + -- A2GDispatcher:SetDefendRadius( 100000 ) + -- + -- -- Set 200km as the radius to defend. + -- A2GDispatcher:SetDefendRadius() -- 200000 is the default value. + -- + function AI_A2G_DISPATCHER:SetDefenseRadius( DefenseRadius ) + + self.DefenseRadius = DefenseRadius or 100000 + + self.Detection:SetAcceptRange( self.DefenseRadius ) + + return self + end + + + + --- Define a border area to simulate a **cold war** scenario. + -- A **cold war** is one where Patrol aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. + -- A **hot war** is one where Patrol aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send Patrol and GCI aircraft to attack it. + -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this. + -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1 + -- @param #AI_A2G_DISPATCHER self + -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Set one ZONE_POLYGON object as the border for the A2G dispatcher. + -- local BorderZone = ZONE_POLYGON( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- The GROUP object is a late activate helicopter unit. + -- A2GDispatcher:SetBorderZone( BorderZone ) + -- + -- or + -- + -- -- Set two ZONE_POLYGON objects as the border for the A2G dispatcher. + -- local BorderZone1 = ZONE_POLYGON( "CCCP Border1", GROUP:FindByName( "CCCP Border1" ) ) -- The GROUP object is a late activate helicopter unit. + -- local BorderZone2 = ZONE_POLYGON( "CCCP Border2", GROUP:FindByName( "CCCP Border2" ) ) -- The GROUP object is a late activate helicopter unit. + -- A2GDispatcher:SetBorderZone( { BorderZone1, BorderZone2 } ) + -- + -- + function AI_A2G_DISPATCHER:SetBorderZone( BorderZone ) + + self.Detection:SetAcceptZones( BorderZone ) + + return self + end + + --- Display a tactical report every 30 seconds about which aircraft are: + -- * Patrolling + -- * Engaging + -- * Returning + -- * Damaged + -- * Out of Fuel + -- * ... + -- @param #AI_A2G_DISPATCHER self + -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the Tactical Display for debug mode. + -- A2GDispatcher:SetTacticalDisplay( true ) + -- + function AI_A2G_DISPATCHER:SetTacticalDisplay( TacticalDisplay ) + + self.TacticalDisplay = TacticalDisplay + + return self + end + + + --- Set the default damage treshold when defenders will RTB. + -- The default damage treshold is by default set to 40%, which means that when the airplane is 40% damaged, it will go RTB. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the %-tage of the damage treshold before going RTB. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default damage treshold. + -- A2GDispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. + -- + function AI_A2G_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold ) + + self.DefenderDefault.DamageThreshold = DamageThreshold + + return self + end + + + --- Set the default Patrol time interval for squadrons, which will be used to determine a random Patrol timing. + -- The default Patrol time interval is between 180 and 600 seconds. + -- @param #AI_A2G_DISPATCHER self + -- @param #number PatrolMinSeconds The minimum amount of seconds for the random time interval. + -- @param #number PatrolMaxSeconds The maximum amount of seconds for the random time interval. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol time interval. + -- A2GDispatcher:SetDefaultPatrolTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. + -- + function AI_A2G_DISPATCHER:SetDefaultPatrolTimeInterval( PatrolMinSeconds, PatrolMaxSeconds ) + + self.DefenderDefault.PatrolMinSeconds = PatrolMinSeconds + self.DefenderDefault.PatrolMaxSeconds = PatrolMaxSeconds + + return self + end + + + --- Set the default Patrol limit for squadrons, which will be used to determine how many Patrol can be airborne at the same time for the squadron. + -- The default Patrol limit is 1 Patrol, which means one Patrol group being spawned. + -- @param #AI_A2G_DISPATCHER self + -- @param #number PatrolLimit The maximum amount of Patrol that can be airborne at the same time for the squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol limit. + -- A2GDispatcher:SetDefaultPatrolLimit( 2 ) -- Maximum 2 Patrol per squadron. + -- + function AI_A2G_DISPATCHER:SetDefaultPatrolLimit( PatrolLimit ) + + self.DefenderDefault.PatrolLimit = PatrolLimit + + return self + end + + + --- Set the default engage limit for squadrons, which will be used to determine how many air units will engage at the same time with the enemy. + -- The default eatrol limit is 1, which means one eatrol group maximum per squadron. + -- @param #AI_A2G_DISPATCHER self + -- @param #number EngageLimit The maximum engages that can be done at the same time per squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol limit. + -- A2GDispatcher:SetDefaultEngageLimit( 2 ) -- Maximum 2 engagements with the enemy per squadron. + -- + function AI_A2G_DISPATCHER:SetDefaultEngageLimit( EngageLimit ) + + self.DefenderDefault.EngageLimit = EngageLimit + + return self + end + + + function AI_A2G_DISPATCHER:SetIntercept( InterceptDelay ) + + self.DefenderDefault.InterceptDelay = InterceptDelay + + local Detection = self.Detection -- Functional.Detection#DETECTION_AREAS + Detection:SetIntercept( true, InterceptDelay ) + + return self + end + + + --- Calculates which defender friendlies are nearby the area, to help protect the area. + -- @param #AI_A2G_DISPATCHER self + -- @param DetectedItem + -- @return #table A list of the defender friendlies nearby, sorted by distance. + function AI_A2G_DISPATCHER:GetDefenderFriendliesNearBy( DetectedItem ) + +-- local DefenderFriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem ) + + local DefenderFriendliesNearBy = {} + + local DetectionCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local ScanZone = ZONE_RADIUS:New( "ScanZone", DetectionCoordinate:GetVec2(), self.DefenseRadius ) + + ScanZone:Scan( Object.Category.UNIT, { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + + local DefenderUnits = ScanZone:GetScannedUnits() + + for DefenderUnitID, DefenderUnit in pairs( DefenderUnits ) do + local DefenderUnit = UNIT:FindByName( DefenderUnit:getName() ) + + DefenderFriendliesNearBy[#DefenderFriendliesNearBy+1] = DefenderUnit + end + + + return DefenderFriendliesNearBy + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTasks() + return self.DefenderTasks or {} + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTask( Defender ) + return self.DefenderTasks[Defender] + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTaskFsm( Defender ) + return self:GetDefenderTask( Defender ).Fsm + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTaskTarget( Defender ) + return self:GetDefenderTask( Defender ).Target + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTaskSquadronName( Defender ) + return self:GetDefenderTask( Defender ).SquadronName + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ClearDefenderTask( Defender ) + if Defender:IsAlive() and self.DefenderTasks[Defender] then + local Target = self.DefenderTasks[Defender].Target + local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() + if Target then + Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + end + self:F( { Target = Message } ) + end + self.DefenderTasks[Defender] = nil + return self + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ClearDefenderTaskTarget( Defender ) + + local DefenderTask = self:GetDefenderTask( Defender ) + + if Defender:IsAlive() and DefenderTask then + local Target = DefenderTask.Target + local Message = "Clearing (" .. DefenderTask.Type .. ") " + Message = Message .. Defender:GetName() + if Target then + Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + end + self:F( { Target = Message } ) + end + if Defender and DefenderTask and DefenderTask.Target then + DefenderTask.Target = nil + end +-- if Defender and DefenderTask then +-- if DefenderTask.Fsm:Is( "Fuel" ) +-- or DefenderTask.Fsm:Is( "LostControl") +-- or DefenderTask.Fsm:Is( "Damaged" ) then +-- self:ClearDefenderTask( Defender ) +-- end +-- end + return self + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target, Size ) + + self:F( { SquadronName = SquadronName, Defender = Defender:GetName() } ) + + self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {} + self.DefenderTasks[Defender].Type = Type + self.DefenderTasks[Defender].Fsm = Fsm + self.DefenderTasks[Defender].SquadronName = SquadronName + self.DefenderTasks[Defender].Size = Size + + if Target then + self:SetDefenderTaskTarget( Defender, Target ) + end + return self + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param Wrapper.Group#GROUP AIGroup + function AI_A2G_DISPATCHER:SetDefenderTaskTarget( Defender, AttackerDetection ) + + local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() + Message = Message .. ( AttackerDetection and ( " target " .. AttackerDetection.Index .. " [" .. AttackerDetection.Set:Count() .. "]" ) ) or "" + self:F( { AttackerDetection = Message } ) + if AttackerDetection then + self.DefenderTasks[Defender].Target = AttackerDetection + end + return self + end + + + --- This is the main method to define Squadrons programmatically. + -- Squadrons: + -- + -- * Have a **name or key** that is the identifier or key of the squadron. + -- * Have **specific plane types** defined by **templates**. + -- * Are **located at one specific airbase**. Multiple squadrons can be located at one airbase through. + -- * Optionally have a limited set of **resources**. The default is that squadrons have unlimited resources. + -- + -- The name of the squadron given acts as the **squadron key** in the AI\_A2G\_DISPATCHER:Squadron...() methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new aircraft are **taking off** from the airfield (in the air, cold, hot, at the runway). + -- * Control how returning aircraft are **landing** at the airfield (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft. + -- + -- @param #AI_A2G_DISPATCHER self + -- + -- @param #string SquadronName A string (text) that defines the squadron identifier or the key of the Squadron. + -- It can be any name, for example `"104th Squadron"` or `"SQ SQUADRON1"`, whatever. + -- As long as you remember that this name becomes the identifier of your squadron you have defined. + -- You need to use this name in other methods too! + -- + -- @param #string AirbaseName The airbase name where you want to have the squadron located. + -- You need to specify here EXACTLY the name of the airbase as you see it in the mission editor. + -- Examples are `"Batumi"` or `"Tbilisi-Lochini"`. + -- EXACTLY the airbase name, between quotes `""`. + -- To ease the airbase naming when using the LDT editor and IntelliSense, the @{Wrapper.Airbase#AIRBASE} class contains enumerations of the airbases of each map. + -- + -- * Caucasus: @{Wrapper.Airbase#AIRBASE.Caucaus} + -- * Nevada or NTTR: @{Wrapper.Airbase#AIRBASE.Nevada} + -- * Normandy: @{Wrapper.Airbase#AIRBASE.Normandy} + -- + -- @param #string TemplatePrefixes A string or an array of strings specifying the **prefix names of the templates** (not going to explain what is templates here again). + -- Examples are `{ "104th", "105th" }` or `"104th"` or `"Template 1"` or `"BLUE PLANES"`. + -- Just remember that your template (groups late activated) need to start with the prefix you have specified in your code. + -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets. + -- + -- @param #number ResourceCount (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. + -- + -- @usage + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- @usage + -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... + -- A2GDispatcher:SetSquadron( "Squadron1", "Batumi", "SQ1", 40 ) + -- + -- @usage + -- -- This will create squadron "Sq 1" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" and has 20 planes in stock... + -- -- Note that in this implementation, the A2G dispatcher will select a random plane type when a new plane (group) needs to be spawned for defenses. + -- -- Note the usage of the {} for the airplane templates list. + -- A2GDispatcher:SetSquadron( "Sq 1", "Batumi", { "Mig-29", "Su-27" }, 40 ) + -- + -- @usage + -- -- This will create 2 squadrons "104th" and "23th" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" respectively and each squadron has 10 planes in stock... + -- A2GDispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 ) + -- A2GDispatcher:SetSquadron( "23th", "Batumi", "Su-27", 10 ) + -- + -- @usage + -- -- This is an example like the previous, but now with infinite resources. + -- -- The ResourceCount parameter is not given in the SetSquadron method. + -- A2GDispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) + -- A2GDispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) + + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + + DefenderSquadron.Name = SquadronName + DefenderSquadron.Airbase = AIRBASE:FindByName( AirbaseName ) + DefenderSquadron.AirbaseName = DefenderSquadron.Airbase:GetName() + if not DefenderSquadron.Airbase then + error( "Cannot find airbase with name:" .. AirbaseName ) + end + + DefenderSquadron.Spawn = {} + if type( TemplatePrefixes ) == "string" then + local SpawnTemplate = TemplatePrefixes + self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) + DefenderSquadron.Spawn[1] = self.DefenderSpawns[SpawnTemplate] + else + for TemplateID, SpawnTemplate in pairs( TemplatePrefixes ) do + self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) + DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate] + end + end + DefenderSquadron.ResourceCount = ResourceCount + DefenderSquadron.TemplatePrefixes = TemplatePrefixes + DefenderSquadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured. + + self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) + + return self + end + + --- Get an item from the Squadron table. + -- @param #AI_A2G_DISPATCHER self + -- @return #table + function AI_A2G_DISPATCHER:GetSquadron( SquadronName ) + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + + if not DefenderSquadron then + error( "Unknown Squadron:" .. SquadronName ) + end + + return DefenderSquadron + end + + + --- Set the Squadron visible before startup of the dispatcher. + -- All planes will be spawned as uncontrolled on the parking spot. + -- They will lock the parking spot. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- A2GDispatcher:SetSquadronVisible( "Mineralnye" ) + -- + function AI_A2G_DISPATCHER:SetSquadronVisible( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.Uncontrolled = true + + for SpawnTemplate, DefenderSpawn in pairs( self.DefenderSpawns ) do + DefenderSpawn:InitUnControlled() + end + + end + + --- Check if the Squadron is visible before startup of the dispatcher. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #bool true if visible. + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- local IsVisible = A2GDispatcher:IsSquadronVisible( "Mineralnye" ) + -- + function AI_A2G_DISPATCHER:IsSquadronVisible( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + return DefenderSquadron.Uncontrolled == true + end + + return nil + + end + + + --- Set the squadron patrol parameters for a specific task type. + -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. + -- + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for SEAD tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for CAS tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for BAI tasks. + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Mineralnye", 2, 30, 60, 1, "SEAD" ) + -- + function AI_A2G_DISPATCHER:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, DefenseTaskType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Patrol = DefenderSquadron[DefenseTaskType] + if Patrol then + Patrol.LowInterval = LowInterval or 180 + Patrol.HighInterval = HighInterval or 600 + Patrol.Probability = Probability or 1 + Patrol.PatrolLimit = PatrolLimit or 1 + Patrol.Scheduler = Patrol.Scheduler or SCHEDULER:New( self ) + local Scheduler = Patrol.Scheduler -- Core.Scheduler#SCHEDULER + local ScheduleID = Patrol.ScheduleID + local Variance = ( Patrol.HighInterval - Patrol.LowInterval ) / 2 + local Repeat = Patrol.LowInterval + Variance + local Randomization = Variance / Repeat + local Start = math.random( 1, Patrol.HighInterval ) + + if ScheduleID then + Scheduler:Stop( ScheduleID ) + end + + Patrol.ScheduleID = Scheduler:Schedule( self, self.SchedulerPatrol, { SquadronName }, Start, Repeat, Randomization ) + else + error( "This squadron does not exist:" .. SquadronName ) + end + + end + + + + --- Set the squadron Patrol parameters for SEAD tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronSeadPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "SEAD" ) + + end + + + --- Set the squadron Patrol parameters for CAS tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronCasPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronCasPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "CAS" ) + + end + + + --- Set the squadron Patrol parameters for BAI tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronBaiPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronBaiPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "BAI" ) + + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:GetPatrolDelay( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Patrol = self.DefenderSquadrons[SquadronName].Patrol or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Patrol = self.DefenderSquadrons[SquadronName].Patrol + if Patrol then + return math.random( Patrol.LowInterval, Patrol.HighInterval ) + else + error( "This squadron does not exist:" .. SquadronName ) + end + end + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #table DefenderSquadron + function AI_A2G_DISPATCHER:CanPatrol( SquadronName, DefenseTaskType ) + self:F({SquadronName = SquadronName}) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron.Captured == false then -- We can only spawn new Patrol if the base has not been captured. + + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + + local Patrol = DefenderSquadron[DefenseTaskType] + if Patrol and Patrol.Patrol == true then + local PatrolCount = self:CountPatrolAirborne( SquadronName, DefenseTaskType ) + self:F( { PatrolCount = PatrolCount, PatrolLimit = Patrol.PatrolLimit, PatrolProbability = Patrol.Probability } ) + if PatrolCount < Patrol.PatrolLimit then + local Probability = math.random() + if Probability <= Patrol.Probability then + return DefenderSquadron, Patrol + end + end + else + self:F( "No patrol for " .. SquadronName ) + end + end + end + return nil + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #table DefenderSquadron + function AI_A2G_DISPATCHER:CanDefend( SquadronName, DefenseTaskType ) + self:F({SquadronName = SquadronName, DefenseTaskType}) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron.Captured == false then -- We can only spawn new defense if the home airbase has not been captured. + + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + if DefenderSquadron[DefenseTaskType] and ( DefenderSquadron[DefenseTaskType].Defend == true ) then + return DefenderSquadron, DefenderSquadron[DefenseTaskType] + end + end + end + return nil + end + + --- Set the squadron engage limit for a specific task type. + -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. + -- + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for SEAD tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for CAS tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for BAI tasks. + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronEngageLimit( "Mineralnye", 2, "SEAD" ) -- Engage maximum 2 groups with the enemy for SEAD defense. + -- + function AI_A2G_DISPATCHER:SetSquadronEngageLimit( SquadronName, EngageLimit, DefenseTaskType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Defense = DefenderSquadron[DefenseTaskType] + if Defense then + Defense.EngageLimit = EngageLimit or 1 + else + error( "This squadron does not exist:" .. SquadronName ) + end + + end + + + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed The minimum speed at which the SEAD task can be executed. + -- @param #number EngageMaxSpeed The maximum speed at which the SEAD task can be executed. + -- @usage + -- + -- -- SEAD Squadron execution. + -- A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200 ) + -- A2GDispatcher:SetSquadronSead( "Novo", 900, 2100 ) + -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronSead( SquadronName, EngageMinSpeed, EngageMaxSpeed ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} + + local Sead = DefenderSquadron.SEAD + Sead.Name = SquadronName + Sead.EngageMinSpeed = EngageMinSpeed + Sead.EngageMaxSpeed = EngageMaxSpeed + Sead.Defend = true + + self:F( { Sead = Sead } ) + end + + --- Set the squadron SEAD engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for SEAD defense. + -- + function AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "SEAD" ) + + end + + + + + --- Set a Sead patrol for a Squadron. + -- The Sead patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Sead Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronSeadPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} + + local SeadPatrol = DefenderSquadron.SEAD + SeadPatrol.Name = SquadronName + SeadPatrol.Zone = Zone + SeadPatrol.FloorAltitude = FloorAltitude + SeadPatrol.CeilingAltitude = CeilingAltitude + SeadPatrol.PatrolMinSpeed = PatrolMinSpeed + SeadPatrol.PatrolMaxSpeed = PatrolMaxSpeed + SeadPatrol.EngageMinSpeed = EngageMinSpeed + SeadPatrol.EngageMaxSpeed = EngageMaxSpeed + SeadPatrol.AltType = AltType + SeadPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "SEAD" ) + + self:F( { Sead = SeadPatrol } ) + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed The minimum speed at which the CAS task can be executed. + -- @param #number EngageMaxSpeed The maximum speed at which the CAS task can be executed. + -- @usage + -- + -- -- CAS Squadron execution. + -- A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200 ) + -- A2GDispatcher:SetSquadronCas( "Novo", 900, 2100 ) + -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronCas( SquadronName, EngageMinSpeed, EngageMaxSpeed ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.CAS = DefenderSquadron.CAS or {} + + local Cas = DefenderSquadron.CAS + Cas.Name = SquadronName + Cas.EngageMinSpeed = EngageMinSpeed + Cas.EngageMaxSpeed = EngageMaxSpeed + Cas.Defend = true + + self:F( { Cas = Cas } ) + end + + + --- Set the squadron CAS engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for CAS defense. + -- + function AI_A2G_DISPATCHER:SetSquadronCasEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "CAS" ) + + end + + + + + --- Set a Cas patrol for a Squadron. + -- The Cas patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Cas Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronCasPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.CAS = DefenderSquadron.CAS or {} + + local CasPatrol = DefenderSquadron.CAS + CasPatrol.Name = SquadronName + CasPatrol.Zone = Zone + CasPatrol.FloorAltitude = FloorAltitude + CasPatrol.CeilingAltitude = CeilingAltitude + CasPatrol.PatrolMinSpeed = PatrolMinSpeed + CasPatrol.PatrolMaxSpeed = PatrolMaxSpeed + CasPatrol.EngageMinSpeed = EngageMinSpeed + CasPatrol.EngageMaxSpeed = EngageMaxSpeed + CasPatrol.AltType = AltType + CasPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "CAS" ) + + self:F( { Cas = CasPatrol } ) + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed The minimum speed at which the BAI task can be executed. + -- @param #number EngageMaxSpeed The maximum speed at which the BAI task can be executed. + -- @usage + -- + -- -- BAI Squadron execution. + -- A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200 ) + -- A2GDispatcher:SetSquadronBai( "Novo", 900, 2100 ) + -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronBai( SquadronName, EngageMinSpeed, EngageMaxSpeed ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.BAI = DefenderSquadron.BAI or {} + + local Bai = DefenderSquadron.BAI + Bai.Name = SquadronName + Bai.EngageMinSpeed = EngageMinSpeed + Bai.EngageMaxSpeed = EngageMaxSpeed + Bai.Defend = true + + self:F( { Bai = Bai } ) + end + + + --- Set the squadron BAI engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for BAI defense. + -- + function AI_A2G_DISPATCHER:SetSquadronBaiEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "BAI" ) + + end + + + --- Set a Bai patrol for a Squadron. + -- The Bai patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Bai Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronBaiPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.BAI = DefenderSquadron.BAI or {} + + local BaiPatrol = DefenderSquadron.BAI + BaiPatrol.Name = SquadronName + BaiPatrol.Zone = Zone + BaiPatrol.FloorAltitude = FloorAltitude + BaiPatrol.CeilingAltitude = CeilingAltitude + BaiPatrol.PatrolMinSpeed = PatrolMinSpeed + BaiPatrol.PatrolMaxSpeed = PatrolMaxSpeed + BaiPatrol.EngageMinSpeed = EngageMinSpeed + BaiPatrol.EngageMaxSpeed = EngageMaxSpeed + BaiPatrol.AltType = AltType + BaiPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "BAI" ) + + self:F( { Bai = BaiPatrol } ) + end + + + --- Defines the default amount of extra planes that will take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- A2GDispatcher:SetDefaultOverhead( 1.5 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultOverhead( Overhead ) + + self.DefenderDefault.Overhead = Overhead + + return self + end + + + --- Defines the amount of extra planes that will take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- A2GDispatcher:SetSquadronOverhead( "SquadronName", 1.5 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Overhead = Overhead + + return self + end + + + --- Gets the overhead of planes as part of the defense system, in comparison with the attackers. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- local SquadronOverhead = A2GDispatcher:GetSquadronOverhead( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:GetSquadronOverhead( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Overhead or self.DefenderDefault.Overhead + end + + + --- Sets the default grouping of new airplanes spawned. + -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set a grouping by default per 2 airplanes. + -- A2GDispatcher:SetDefaultGrouping( 2 ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultGrouping( Grouping ) + + self.DefenderDefault.Grouping = Grouping + + return self + end + + + --- Sets the grouping of new airplanes spawned. + -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set a grouping per 2 airplanes. + -- A2GDispatcher:SetSquadronGrouping( "SquadronName", 2 ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Grouping = Grouping + + return self + end + + + --- Defines the default method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Air ) + -- + -- -- Let new flights by default take-off from the runway. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Runway ) + -- + -- -- Let new flights by default take-off from the airbase hot. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Hot ) + -- + -- -- Let new flights by default take-off from the airbase cold. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Cold ) + -- + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoff( Takeoff ) + + self.DefenderDefault.Takeoff = Takeoff + + return self + end + + --- Defines the method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Air ) + -- + -- -- Let new flights take-off from the runway. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Runway ) + -- + -- -- Let new flights take-off from the airbase hot. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Hot ) + -- + -- -- Let new flights take-off from the airbase cold. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Cold ) + -- + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Takeoff = Takeoff + + return self + end + + + --- Gets the default method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- local TakeoffMethod = A2GDispatcher:GetDefaultTakeoff() + -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetDefaultTakeoff( ) + + return self.DefenderDefault.Takeoff + end + + --- Gets the method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- local TakeoffMethod = A2GDispatcher:GetSquadronTakeoff( "SquadronName" ) + -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetSquadronTakeoff( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff + end + + + --- Sets flights to default take-off in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- A2GDispatcher:SetDefaultTakeoffInAir() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffInAir() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Air ) + + return self + end + + + --- Sets flights to take-off in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number TakeoffAltitude (optional) The altitude in meters above the ground. If not given, the default takeoff altitude will be used. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoffInAir( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Air ) + + if TakeoffAltitude then + self:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) + end + + return self + end + + + --- Sets flights by default to take-off from the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off from the runway. + -- A2GDispatcher:SetDefaultTakeoffFromRunway() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffFromRunway() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Runway ) + + return self + end + + + --- Sets flights to take-off from the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from the runway. + -- A2GDispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Runway ) + + return self + end + + + --- Sets flights by default to take-off from the airbase at a hot location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off at a hot parking spot. + -- A2GDispatcher:SetDefaultTakeoffFromParkingHot() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingHot() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Hot ) + + return self + end + + --- Sets flights to take-off from the airbase at a hot location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Hot ) + + return self + end + + + --- Sets flights to by default take-off from the airbase at a cold location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- A2GDispatcher:SetDefaultTakeoffFromParkingCold() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingCold() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Cold ) + + return self + end + + + --- Sets flights to take-off from the airbase at a cold location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- A2GDispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Cold ) + + return self + end + + + --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + -- @param #AI_A2G_DISPATCHER self + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set the default takeoff altitude when taking off in the air. + -- A2GDispatcher:SetDefaultTakeoffInAirAltitude( 2000 ) -- This makes planes start at 2000 meters above the ground. + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude ) + + self.DefenderDefault.TakeoffAltitude = TakeoffAltitude + + return self + end + + --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set the default takeoff altitude when taking off in the air. + -- A2GDispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.TakeoffAltitude = TakeoffAltitude + + return self + end + + + --- Defines the default method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default despawn near the airbase when returning. + -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.NearAirbase ) + -- + -- -- Let new flights by default despawn after landing land at the runway. + -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtRunway ) + -- + -- -- Let new flights by default despawn after landing and parking, and after engine shutdown. + -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtEngineShutdown ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLanding( Landing ) + + self.DefenderDefault.Landing = Landing + + return self + end + + + --- Defines the method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights despawn near the airbase when returning. + -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.NearAirbase ) + -- + -- -- Let new flights despawn after landing land at the runway. + -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtRunway ) + -- + -- -- Let new flights despawn after landing and parking, and after engine shutdown. + -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtEngineShutdown ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLanding( SquadronName, Landing ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Landing = Landing + + return self + end + + + --- Gets the default method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default despawn near the airbase when returning. + -- local LandingMethod = A2GDispatcher:GetDefaultLanding( AI_A2G_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_A2G_Dispatcher.Landing.NearAirbase then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetDefaultLanding() + + return self.DefenderDefault.Landing + end + + + --- Gets the method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights despawn near the airbase when returning. + -- local LandingMethod = A2GDispatcher:GetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_A2G_Dispatcher.Landing.NearAirbase then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetSquadronLanding( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Landing or self.DefenderDefault.Landing + end + + + --- Sets flights by default to land and despawn near the airbase in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights by default to land near the airbase and despawn. + -- A2GDispatcher:SetDefaultLandingNearAirbase() + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLandingNearAirbase() + + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.NearAirbase ) + + return self + end + + + --- Sets flights to land and despawn near the airbase in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights to land near the airbase and despawn. + -- A2GDispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.NearAirbase ) + + return self + end + + + --- Sets flights by default to land and despawn at the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land at the runway and despawn. + -- A2GDispatcher:SetDefaultLandingAtRunway() + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLandingAtRunway() + + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.AtRunway ) + + return self + end + + + --- Sets flights to land and despawn at the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights land at the runway and despawn. + -- A2GDispatcher:SetSquadronLandingAtRunway( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.AtRunway ) + + return self + end + + + --- Sets flights by default to land and despawn at engine shutdown, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land and despawn at engine shutdown. + -- A2GDispatcher:SetDefaultLandingAtEngineShutdown() + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLandingAtEngineShutdown() + + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.AtEngineShutdown ) + + return self + end + + + --- Sets flights to land and despawn at engine shutdown, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights land and despawn at engine shutdown. + -- A2GDispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.AtEngineShutdown ) + + return self + end + + --- Set the default fuel treshold when defenders will RTB or Refuel in the air. + -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + -- @param #AI_A2G_DISPATCHER self + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + function AI_A2G_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) + + self.DefenderDefault.FuelThreshold = FuelThreshold + + return self + end + + + --- Set the fuel treshold for the squadron when defenders will RTB or Refuel in the air. + -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + function AI_A2G_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.FuelThreshold = FuelThreshold + + return self + end + + --- Set the default tanker where defenders will Refuel in the air. + -- @param #AI_A2G_DISPATCHER self + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + -- -- Now Setup the default tanker. + -- A2GDispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + function AI_A2G_DISPATCHER:SetDefaultTanker( TankerName ) + + self.DefenderDefault.TankerName = TankerName + + return self + end + + + --- Set the squadron tanker where defenders will Refuel in the air. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the squadron fuel treshold. + -- A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + -- -- Now Setup the squadron tanker. + -- A2GDispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + function AI_A2G_DISPATCHER:SetSquadronTanker( SquadronName, TankerName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.TankerName = TankerName + + return self + end + + + + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + self.Defenders[ DefenderName ] = Squadron + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount - Size + end + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount + Defender:GetSize() + end + self.Defenders[ DefenderName ] = nil + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + end + + function AI_A2G_DISPATCHER:GetSquadronFromDefender( Defender ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + self:F( { DefenderName = DefenderName } ) + return self.Defenders[ DefenderName ] + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:CountPatrolAirborne( SquadronName, DefenseTaskType ) + + local PatrolCount = 0 + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + if DefenderSquadron then + for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do + if DefenderTask.SquadronName == SquadronName then + if DefenderTask.Type == DefenseTaskType then + if AIGroup:IsAlive() then + -- Check if the Patrol is patrolling or engaging. If not, this is not a valid Patrol, even if it is alive! + -- The Patrol could be damaged, lost control, or out of fuel! + if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) + or DefenderTask.Fsm:Is( "Started" ) then + PatrolCount = PatrolCount + 1 + end + end + end + end + end + end + + return PatrolCount + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:CountDefendersEngaged( AttackerDetection ) + + -- First, count the active AIGroups Units, targetting the DetectedSet + local DefendersEngaged = 0 + local DefendersTotal = 0 + + local AttackerSet = AttackerDetection.Set + local AttackerCount = AttackerSet:Count() + local DefendersMissing = AttackerCount + --DetectedSet:Flush() + + local DefenderTasks = self:GetDefenderTasks() + for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do + local Defender = DefenderGroup -- Wrapper.Group#GROUP + local DefenderTaskTarget = DefenderTask.Target + local DefenderSquadronName = DefenderTask.SquadronName + local DefenderSize = DefenderTask.Size + + -- Count the total of defenders on the battlefield. + --local DefenderSize = Defender:GetInitialSize() + if DefenderTask.Target then + --if DefenderTask.Fsm:Is( "Engaging" ) then + self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + DefendersTotal = DefendersTotal + DefenderSize + if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then + + local SquadronOverhead = self:GetSquadronOverhead( DefenderSquadronName ) + if DefenderSize then + DefendersEngaged = DefendersEngaged + DefenderSize + DefendersMissing = DefendersMissing - DefenderSize / SquadronOverhead + self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + else + DefendersEngaged = 0 + end + end + --end + end + + + end + + self:F( { DefenderCount = DefendersEngaged } ) + + return DefendersTotal, DefendersEngaged, DefendersMissing + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:CountDefenders( AttackerDetection, DefenderCount, DefenderTaskType ) + + local Friendlies = nil + + local AttackerSet = AttackerDetection.Set + local AttackerCount = AttackerSet:Count() + + local DefenderFriendlies = self:GetDefenderFriendliesNearBy( AttackerDetection ) + + for FriendlyDistance, DefenderFriendlyUnit in UTILS.spairs( DefenderFriendlies or {} ) do + -- We only allow to engage targets as long as the units on both sides are balanced. + if AttackerCount > DefenderCount then + local FriendlyGroup = DefenderFriendlyUnit:GetGroup() -- Wrapper.Group#GROUP + if FriendlyGroup and FriendlyGroup:IsAlive() then + -- Ok, so we have a friendly near the potential target. + -- Now we need to check if the AIGroup has a Task. + local DefenderTask = self:GetDefenderTask( FriendlyGroup ) + if DefenderTask then + -- The Task should be of the same type. + if DefenderTaskType == DefenderTask.Type then + -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet + if DefenderTask.Target == nil then + if DefenderTask.Fsm:Is( "Returning" ) + or DefenderTask.Fsm:Is( "Patrolling" ) then + Friendlies = Friendlies or {} + Friendlies[FriendlyGroup] = FriendlyGroup + DefenderCount = DefenderCount + FriendlyGroup:GetSize() + self:F( { Friendly = FriendlyGroup:GetName(), FriendlyDistance = FriendlyDistance } ) + end + end + end + end + end + else + break + end + end + + return Friendlies + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + local SquadronName = DefenderSquadron.Name + DefendersNeeded = DefendersNeeded or 4 + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded + + if self:IsSquadronVisible( SquadronName ) then + + -- Here we Patrol the new planes. + -- The Resources table is filled in advance. + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template. + + -- We determine the grouping based on the parameters set. + self:F( { DefenderGrouping = DefenderGrouping } ) + + -- New we will form the group to spawn in. + -- We search for the first free resource matching the template. + local DefenderUnitIndex = 1 + local DefenderPatrolTemplate = nil + local DefenderName = nil + for GroupName, DefenderGroup in pairs( DefenderSquadron.Resources[TemplateID] or {} ) do + self:F( { GroupName = GroupName } ) + local DefenderTemplate = _DATABASE:GetGroupTemplate( GroupName ) + if DefenderUnitIndex == 1 then + DefenderPatrolTemplate = UTILS.DeepCopy( DefenderTemplate ) + self.DefenderPatrolIndex = self.DefenderPatrolIndex + 1 + DefenderPatrolTemplate.name = SquadronName .. "#" .. self.DefenderPatrolIndex .. "#" .. GroupName + DefenderName = DefenderPatrolTemplate.name + else + -- Add the unit in the template to the DefenderPatrolTemplate. + local DefenderUnitTemplate = DefenderTemplate.units[1] + DefenderPatrolTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate + end + DefenderUnitIndex = DefenderUnitIndex + 1 + DefenderSquadron.Resources[TemplateID][GroupName] = nil + if DefenderUnitIndex > DefenderGrouping then + break + end + + end + + if DefenderPatrolTemplate then + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local SpawnGroup = GROUP:Register( DefenderName ) + DefenderPatrolTemplate.lateActivation = nil + DefenderPatrolTemplate.uncontrolled = nil + local Takeoff = self:GetSquadronTakeoff( SquadronName ) + DefenderPatrolTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + DefenderPatrolTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + local Defender = _DATABASE:Spawn( DefenderPatrolTemplate ) + + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + return Defender, DefenderGrouping + end + else + local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN + if DefenderGrouping then + Spawn:InitGrouping( DefenderGrouping ) + else + Spawn:InitGrouping() + end + + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local Defender = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + return Defender, DefenderGrouping + end + + return nil, nil + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterPatrol( From, Event, To, SquadronName, DefenseTaskType ) + + self:F({SquadronName = SquadronName}) + + local DefenderSquadron, Patrol = self:CanPatrol( SquadronName, DefenseTaskType ) + + if Patrol then + + local DefenderPatrol, DefenderGrouping = self:ResourceActivate( DefenderSquadron ) + + if DefenderPatrol then + + local Fsm = AI_A2G_PATROL:New( DefenderPatrol, Patrol.Zone, Patrol.FloorAltitude, Patrol.CeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.AltType ) + Fsm:SetDispatcher( self ) + Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + Fsm:SetDisengageRadius( self.DisengageRadius ) + Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) + Fsm:Start() + + self:SetDefenderTask( SquadronName, DefenderPatrol, DefenseTaskType, Fsm ) + + function Fsm:onafterTakeoff( Defender, From, Event, To ) + self:F({"Patrol Birth", Defender:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + + if Squadron then + Fsm:__Patrol( 2 ) -- Start Patrolling + end + end + + function Fsm:onafterRTB( Defender, From, Event, To ) + self:F({"Patrol RTB", Defender:GetName()}) + self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + Dispatcher:ClearDefenderTaskTarget( Defender ) + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterHome( Defender, From, Event, To, Action ) + self:F({"Patrol Home", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + self:ParkDefender( Squadron, Defender ) + end + end + end + end + + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterEngage( From, Event, To, AttackerDetection, Defenders ) + + if Defenders then + + for DefenderID, Defender in pairs( Defenders or {} ) do + + local Fsm = self:GetDefenderTaskFsm( Defender ) + Fsm:__Engage( 1, AttackerDetection.Set ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( Defender, AttackerDetection ) + + end + end + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterDefend( From, Event, To, AttackerDetection, DefendersTotal, DefendersEngaged, DefendersMissing, DefenderFriendlies, DefenseTaskType ) + + self:F( { From, Event, To, AttackerDetection.Index, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing, DefenderFriendlies = DefenderFriendlies } ) + + local AttackerSet = AttackerDetection.Set + local AttackerUnit = AttackerSet:GetFirst() + + if AttackerUnit and AttackerUnit:IsAlive() then + local AttackerCount = AttackerSet:Count() + local DefenderCount = 0 + + for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do + + local SquadronName = self:GetDefenderTask( DefenderGroup ).SquadronName + local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) + + local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) + Fsm:__Engage( 1, AttackerSet ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) + + local DefenderGroupSize = DefenderGroup:GetSize() + DefendersMissing = DefendersMissing - DefenderGroupSize / SquadronOverhead + DefendersTotal = DefendersTotal + DefenderGroupSize / SquadronOverhead + + if DefendersMissing <= 0 then + break + end + end + + self:F( { DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) + DefenderCount = DefendersMissing + + local ClosestDistance = 0 + local ClosestDefenderSquadronName = nil + + local BreakLoop = false + + while( DefenderCount > 0 and not BreakLoop ) do + + self:F( { DefenderSquadrons = self.DefenderSquadrons } ) + + for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons or {} ) do + + if DefenderSquadron[DefenseTaskType] then + + local SpawnCoord = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE + local AttackerCoord = AttackerUnit:GetCoordinate() + local InterceptCoord = AttackerDetection.InterceptCoord + self:F( { InterceptCoord = InterceptCoord } ) + if InterceptCoord then + local InterceptDistance = SpawnCoord:Get2DDistance( InterceptCoord ) + local AirbaseDistance = SpawnCoord:Get2DDistance( AttackerCoord ) + self:F( { InterceptDistance = InterceptDistance, AirbaseDistance = AirbaseDistance, InterceptCoord = InterceptCoord } ) + + if ClosestDistance == 0 or InterceptDistance < ClosestDistance then + + -- Only intercept if the distance to target is smaller or equal to the GciRadius limit. + if AirbaseDistance <= self.DefenseRadius then + ClosestDistance = InterceptDistance + ClosestDefenderSquadronName = SquadronName + end + end + end + end + end + + if ClosestDefenderSquadronName then + + local DefenderSquadron, Defense = self:CanDefend( ClosestDefenderSquadronName, DefenseTaskType ) + + if Defense then + + local DefenderOverhead = DefenderSquadron.Overhead or self.DefenderDefault.Overhead + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + local DefendersNeeded = math.ceil( DefenderCount * DefenderOverhead ) + + self:F( { Overhead = DefenderOverhead, SquadronOverhead = DefenderSquadron.Overhead , DefaultOverhead = self.DefenderDefault.Overhead } ) + self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } ) + self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } ) + + -- Validate that the maximum limit of Defenders has been reached. + -- If yes, then cancel the engaging of more defenders. + local DefendersLimit = DefenderSquadron.EngageLimit or self.DefenderDefault.EngageLimit + if DefendersLimit then + if DefendersTotal >= DefendersLimit then + DefendersNeeded = 0 + BreakLoop = true + else + -- If the total of amount of defenders + the defenders needed, is larger than the limit of defenders, + -- then the defenders needed is the difference between defenders total - defenders limit. + if DefendersTotal + DefendersNeeded > DefendersLimit then + DefendersNeeded = DefendersLimit - DefendersTotal + end + end + end + + -- DefenderSquadron.ResourceCount can have the value nil, which expresses unlimited resources. + -- DefendersNeeded cannot exceed DefenderSquadron.ResourceCount! + if DefenderSquadron.ResourceCount and DefendersNeeded > DefenderSquadron.ResourceCount then + DefendersNeeded = DefenderSquadron.ResourceCount + BreakLoop = true + end + + while ( DefendersNeeded > 0 ) do + + local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + DefendersNeeded = DefendersNeeded - DefenderGrouping + + if DefenderGroup then + + DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead + + local Fsm = AI_A2G_ENGAGE:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed ) + Fsm:SetDispatcher( self ) + Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + Fsm:SetDisengageRadius( self.DisengageRadius ) + Fsm:Start() + + self:SetDefenderTask( ClosestDefenderSquadronName, DefenderGroup, DefenseTaskType, Fsm, AttackerDetection, DefenderGrouping ) + + function Fsm:onafterTakeoff( Defender, From, Event, To ) + self:F({"Defender Birth", Defender:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) + + if DefenderTarget then + Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit + end + end + + function Fsm:onafterRTB( Defender, From, Event, To ) + self:F({"Defender RTB", Defender:GetName()}) + self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + Dispatcher:ClearDefenderTaskTarget( Defender ) + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterLostControl( Defender, From, Event, To ) + self:F({"Defender LostControl", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + if Defender:IsAboveRunway() then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterHome( Defender, From, Event, To, Action ) + self:F({"Defender Home", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + self:ParkDefender( Squadron, Defender ) + end + end + end -- if DefenderGCI then + end -- while ( DefendersNeeded > 0 ) do + else + -- No more resources, try something else. + -- Subject for a later enhancement to try to depart from another squadron and disable this one. + BreakLoop = true + break + end + else + -- There isn't any closest airbase anymore, break the loop. + break + end + end -- if DefenderSquadron then + end -- if AttackerUnit + end + + + + --- Creates an SEAD task when the targets have radars. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. + -- @return #nil If there are no targets to be set. + function AI_A2G_DISPATCHER:Evaluate_SEAD( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT + local AttackerCount = AttackerSet:Count() + local IsSEAD = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group? + + if ( IsSEAD > 0 ) then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) + + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "SEAD" ) + + if DetectedItem.IsDetected == true then + + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups + end + end + + return nil, nil, nil + end + + + --- Creates an CAS task. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. + -- @return #nil If there are no targets to be set. + function AI_A2G_DISPATCHER:Evaluate_CAS( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT + local AttackerCount = AttackerSet:Count() + local AttackerRadarCount = AttackerSet:HasSEAD() + local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + local IsCas = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == true ) -- Is the AttackerSet a CAS group? + + self:F( { Friendlies = self.Detection:GetFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) } ) + + if IsCas == true then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) + + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "CAS" ) + + if DetectedItem.IsDetected == true then + + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups + end + end + + return nil, nil, nil + end + + + --- Evaluates an BAI task. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. + -- @return #nil If there are no targets to be set. + function AI_A2G_DISPATCHER:Evaluate_BAI( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT + local AttackerCount = AttackerSet:Count() + local AttackerRadarCount = AttackerSet:HasSEAD() + local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + local IsBai = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == false ) -- Is the AttackerSet a BAI group? + + if IsBai == true then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) + + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "BAI" ) + + if DetectedItem.IsDetected == true then + + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups + end + end + + return nil, nil, nil + end + + + --- Assigns A2G AI Tasks in relation to the detected items. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function AI_A2G_DISPATCHER:ProcessDetected( Detection ) + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local TaskReport = REPORT:New() + + + for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do + local DefenderGroup = DefenderGroup -- Wrapper.Group#GROUP + if not DefenderGroup:IsAlive() then + local DefenderTaskFsm = self:GetDefenderTaskFsm( DefenderGroup ) + self:F( { Defender = DefenderGroup:GetName(), DefenderState = DefenderTaskFsm:GetState() } ) + if not DefenderTaskFsm:Is( "Started" ) then + self:ClearDefenderTask( DefenderGroup ) + end + else + if DefenderTask.Target then + local AttackerItem = Detection:GetDetectedItemByIndex( DefenderTask.Target.Index ) + if not AttackerItem then + self:F( { "Removing obsolete Target:", DefenderTask.Target.Index } ) + self:ClearDefenderTaskTarget( DefenderGroup ) + else + if DefenderTask.Target.Set then + local AttackerCount = DefenderTask.Target.Set:Count() + if AttackerCount == 0 then + self:F( { "All Targets destroyed in Target, removing:", DefenderTask.Target.Index } ) + self:ClearDefenderTaskTarget( DefenderGroup ) + end + end + end + end + end + end + + local Report = REPORT:New( "\nTactical Overview" ) + + local DefenderGroupCount = 0 + local Delay = 0 -- We need to implement a delay for each action because the spawning on airbases get confused if done too quick. + + local DefendersTotal = 0 + + -- Now that all obsolete tasks are removed, loop through the detected targets. + for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedCount = DetectedSet:Count() + local DetectedZone = DetectedItem.Zone + + self:F( { "Target ID", DetectedItem.ItemID } ) + DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local DetectionIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + local AttackerCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + -- Calculate if for this DetectedItem if a defense needs to be initiated. + -- This calculation is based on the distance between the defense point and the attackers, and the defensiveness parameter. + -- The attackers closest to the defense coordinates will be handled first, or course! + + local DefenseCoordinate = nil + + for DefenseCoordinateName, EvaluateCoordinate in pairs( self.DefenseCoordinates ) do + + local EvaluateDistance = AttackerCoordinate:Get2DDistance( EvaluateCoordinate ) + + if EvaluateDistance <= self.DefenseRadius then + + local DistanceProbability = ( self.DefenseRadius / EvaluateDistance * self.DefenseReactivity ) + local DefenseProbability = math.random() + + self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) + + if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then + DefenseCoordinate = EvaluateCoordinate + break + end + end + end + + if DefenseCoordinate then + do + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_SEAD( DetectedItem ) -- Returns a SET_UNIT with the SEAD targets to be engaged... + if DefendersMissing and DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "SEAD", DefenseCoordinate ) + Delay = Delay + 1 + end + end + + do + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... + if DefendersMissing and DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "CAS", DefenseCoordinate ) + Delay = Delay + 1 + end + end + + do + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_BAI( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... + if DefendersMissing and DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "BAI", DefenseCoordinate ) + Delay = Delay + 1 + end + end + end + +-- do +-- local DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) +-- if DefendersMissing and DefendersMissing > 0 then +-- self:F( { DefendersMissing = DefendersMissing } ) +-- self:CAS( DetectedItem, DefendersMissing, Friendlies ) +-- end +-- end + + if self.TacticalDisplay then + -- Show tactical situation + Report:Add( string.format( "\n - Target %s ( %s ): ( #%d ) %s" , DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) ) + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + local Defender = Defender -- Wrapper.Group#GROUP + if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then + if Defender:IsAlive() then + DefenderGroupCount = DefenderGroupCount + 1 + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + end + end + + if self.TacticalDisplay then + Report:Add( "\n - No Targets:") + local TaskCount = 0 + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + TaskCount = TaskCount + 1 + local Defender = Defender -- Wrapper.Group#GROUP + if not DefenderTask.Target then + if Defender:IsAlive() then + local DefenderHasTask = Defender:HasTask() + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + DefenderGroupCount = DefenderGroupCount + 1 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) + + self:F( Report:Text( "\n" ) ) + trigger.action.outText( Report:Text( "\n" ), 25 ) + end + + return true + end + +end + +do + + --- Calculates which HUMAN friendlies are nearby the area. + -- @param #AI_A2G_DISPATCHER self + -- @param DetectedItem The detected item. + -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. + function AI_A2G_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem ) + + local PlayerTypes = {} + local PlayersCount = 0 + + if PlayersNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do + local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT + local PlayerName = PlayerUnit:GetPlayerName() + --self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) + if PlayerUnit:IsAirPlane() and PlayerName ~= nil then + local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel() + PlayersCount = PlayersCount + 1 + local PlayerType = PlayerUnit:GetTypeName() + PlayerTypes[PlayerName] = PlayerType + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + --self:F( { PlayersCount = PlayersCount } ) + + local PlayerTypesReport = REPORT:New() + + if PlayersCount > 0 then + for PlayerName, PlayerType in pairs( PlayerTypes ) do + PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) ) + end + else + PlayerTypesReport:Add( "-" ) + end + + + return PlayersCount, PlayerTypesReport + end + + --- Calculates which friendlies are nearby the area. + -- @param #AI_A2G_DISPATCHER self + -- @param DetectedItem The detected item. + -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. + function AI_A2G_DISPATCHER:GetFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem ) + + local FriendlyTypes = {} + local FriendliesCount = 0 + + if FriendlyUnitsNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do + local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT + if FriendlyUnit:IsAirPlane() then + local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel() + FriendliesCount = FriendliesCount + 1 + local FriendlyType = FriendlyUnit:GetTypeName() + FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1 + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + --self:F( { FriendliesCount = FriendliesCount } ) + + local FriendlyTypesReport = REPORT:New() + + if FriendliesCount > 0 then + for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do + FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) ) + end + else + FriendlyTypesReport:Add( "-" ) + end + + + return FriendliesCount, FriendlyTypesReport + end + + --- Schedules a new Patrol for the given SquadronName. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + function AI_A2G_DISPATCHER:SchedulerPatrol( SquadronName ) + local PatrolTaskTypes = { "SEAD", "CAS", "BAI" } + local PatrolTaskType = PatrolTaskTypes[math.random(1,3)] + self:Patrol( SquadronName, PatrolTaskType ) + end + +end + +do + + --- @type AI_A2G_GCICAP + -- @extends #AI_A2G_DISPATCHER + + --- Create an automatic air defence system for a coalition setting up GCI and CAP air defenses. + -- The class derives from @{#AI_A2G_DISPATCHER} and thus, all the methods that are defined in the @{#AI_A2G_DISPATCHER} class, can be used also in AI\_A2G\_GCICAP. + -- + -- === + -- + -- # Demo Missions + -- + -- ### [AI\_A2G\_GCICAP for Caucasus](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-200%20-%20AI_A2G%20-%20GCICAP%20Demonstration) + -- ### [AI\_A2G\_GCICAP for NTTR](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-210%20-%20NTTR%20AI_A2G_GCICAP%20Demonstration) + -- ### [AI\_A2G\_GCICAP for Normandy](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-220%20-%20NORMANDY%20AI_A2G_GCICAP%20Demonstration) + -- + -- ### [AI\_A2G\_GCICAP for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching) + -- + -- === + -- + -- # YouTube Channel + -- + -- ### [DCS WORLD - MOOSE - A2G GCICAP - Build an automatic A2G Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) + -- + -- === + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia3.JPG) + -- + -- AI\_A2G\_GCICAP includes automatic spawning of Combat Air Patrol aircraft (CAP) and Ground Controlled Intercept aircraft (GCI) in response to enemy + -- air movements that are detected by an airborne or ground based radar network. + -- + -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system. + -- + -- The AI_A2G_GCICAP provides a lightweight configuration method using the mission editor. Within a very short time, and with very little coding, + -- the mission designer is able to configure a complete A2G defense system for a coalition using the DCS Mission Editor available functions. + -- Using the DCS Mission Editor, you define borders of the coalition which are guarded by GCICAP, + -- configure airbases to belong to the coalition, define squadrons flying certain types of planes or payloads per airbase, and define CAP zones. + -- **Very little lua needs to be applied, a one liner**, which is fully explained below, which can be embedded + -- right in a DO SCRIPT trigger action or in a larger DO SCRIPT FILE trigger action. + -- + -- CAP flights will take off and proceed to designated CAP zones where they will remain on station until the ground radars direct them to intercept + -- detected enemy aircraft or they run short of fuel and must return to base (RTB). + -- + -- When a CAP flight leaves their zone to perform a GCI or return to base a new CAP flight will spawn to take its place. + -- If all CAP flights are engaged or RTB then additional GCI interceptors will scramble to intercept unengaged enemy aircraft under ground radar control. + -- + -- In short it is a plug in very flexible and configurable air defence module for DCS World. + -- + -- === + -- + -- # The following actions need to be followed when using AI\_A2G\_GCICAP in your mission: + -- + -- ## 1) Configure a working AI\_A2G\_GCICAP defense system for ONE coalition. + -- + -- ### 1.1) Define which airbases are for which coalition. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_1.JPG) + -- + -- Color the airbases red or blue. You can do this by selecting the airbase on the map, and select the coalition blue or red. + -- + -- ### 1.2) Place groups of units given a name starting with a **EWR prefix** of your choice to build your EWR network. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_2.JPG) + -- + -- **All EWR groups starting with the EWR prefix (text) will be included in the detection system.** + -- + -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy. + -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. + -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). + -- Additionally, ANY other radar capable unit can be part of the EWR network! + -- Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. + -- The position of these units is very important as they need to provide enough coverage + -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. + -- + -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. + -- For example if they are a long way forward and can detect enemy planes on the ground and taking off + -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. + -- Having the radars further back will mean a slower escalation because fewer targets will be detected and + -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. + -- It all depends on what the desired effect is. + -- + -- EWR networks are **dynamically maintained**. By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, + -- increasing or decreasing the radar coverage of the Early Warning System. + -- + -- ### 1.3) Place Airplane or Helicopter Groups with late activation switched on above the airbases to define Squadrons. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_3.JPG) + -- + -- These are **templates**, with a given name starting with a **Template prefix** above each airbase that you wanna have a squadron. + -- These **templates** need to be within 1.5km from the airbase center. They don't need to have a slot at the airplane, they can just be positioned above the airbase, + -- without a route, and should only have ONE unit. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_4.JPG) + -- + -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** + -- + -- ### 1.4) Place floating helicopters to create the CAP zones defined by its route points. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_5.JPG) + -- + -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** + -- + -- The helicopter indicates the start of the CAP zone. + -- The route points define the form of the CAP zone polygon. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_6.JPG) + -- + -- **The place of the helicopter is important, as the airbase closest to the helicopter will be the airbase from where the CAP planes will take off for CAP.** + -- + -- ## 2) There are a lot of defaults set, which can be further modified using the methods in @{#AI_A2G_DISPATCHER}: + -- + -- ### 2.1) Planes are taking off in the air from the airbases. + -- + -- This prevents airbases to get cluttered with airplanes taking off, it also reduces the risk of human players colliding with taxiiing airplanes, + -- resulting in the airbase to halt operations. + -- + -- You can change the way how planes take off by using the inherited methods from AI\_A2G\_DISPATCHER: + -- + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. + -- + -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. + -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: + -- + -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. + -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. + -- * aircraft may collide at the airbase. + -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... + -- + -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. + -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! + -- + -- ### 2.2) Planes return near the airbase or will land if damaged. + -- + -- When damaged airplanes return to the airbase, they will be routed and will dissapear in the air when they are near the airbase. + -- There are exceptions to this rule, airplanes that aren't "listening" anymore due to damage or out of fuel, will return to the airbase and land. + -- + -- You can change the way how planes land by using the inherited methods from AI\_A2G\_DISPATCHER: + -- + -- * @{#AI_A2G_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. + -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the + -- A2G defense system, as no new CAP or GCI planes can takeoff. + -- Note that the method @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. + -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. + -- + -- ### 2.3) CAP operations setup for specific airbases, will be executed with the following parameters: + -- + -- * The altitude will range between 6000 and 10000 meters. + -- * The CAP speed will vary between 500 and 800 km/h. + -- * The engage speed between 800 and 1200 km/h. + -- + -- You can change or add a CAP zone by using the inherited methods from AI\_A2G\_DISPATCHER: + -- + -- The method @{#AI_A2G_DISPATCHER.SetSquadronPatrol}() defines a CAP execution for a squadron. + -- + -- Setting-up a CAP zone also requires specific parameters: + -- + -- * The minimum and maximum altitude + -- * The minimum speed and maximum patrol speed + -- * The minimum and maximum engage speed + -- * The type of altitude measurement + -- + -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. + -- + -- The @{#AI_A2G_DISPATCHER.SetSquadronPatrolInterval}() method specifies **how much** and **when** CAP flights will takeoff. + -- + -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system. + -- + -- For example, the following setup will create a CAP for squadron "Sochi": + -- + -- A2GDispatcher:SetSquadronPatrol( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Sochi", 2, 30, 120, 1 ) + -- + -- ### 2.4) Each airbase will perform GCI when required, with the following parameters: + -- + -- * The engage speed is between 800 and 1200 km/h. + -- + -- You can change or add a GCI parameters by using the inherited methods from AI\_A2G\_DISPATCHER: + -- + -- The method @{#AI_A2G_DISPATCHER.SetSquadronGci}() defines a GCI execution for a squadron. + -- + -- Setting-up a GCI readiness also requires specific parameters: + -- + -- * The minimum speed and maximum patrol speed + -- + -- Essentially this controls how many flights of GCI aircraft can be active at any time. + -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers. + -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, + -- too short will mean that the intruders may have alraedy passed the ideal interception point! + -- + -- For example, the following setup will create a GCI for squadron "Sochi": + -- + -- A2GDispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) + -- + -- ### 2.5) Grouping or detected targets. + -- + -- Detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate + -- group being detected. + -- + -- Targets will be grouped within a radius of 30km by default. + -- + -- The radius indicates that detected targets need to be grouped within a radius of 30km. + -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. + -- Fast planes like in the 80s, need a larger radius than WWII planes. + -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft. + -- + -- ## 3) Additional notes: + -- + -- In order to create a two way A2G defense system, **two AI\_A2G\_GCICAP defense systems must need to be created**, for each coalition one. + -- Each defense system needs its own EWR network setup, airplane templates and CAP configurations. + -- + -- This is a good implementation, because maybe in the future, more coalitions may become available in DCS world. + -- + -- ## 4) Coding examples how to use the AI\_A2G\_GCICAP class: + -- + -- ### 4.1) An easy setup: + -- + -- -- Setup the AI_A2G_GCICAP dispatcher for one coalition, and initialize it. + -- GCI_Red = AI_A2G_GCICAP:New( "EWR CCCP", "SQUADRON CCCP", "CAP CCCP", 2 ) + -- -- + -- The following parameters were given to the :New method of AI_A2G_GCICAP, and mean the following: + -- + -- * `"EWR CCCP"`: Groups of the blue coalition are placed that define the EWR network. These groups start with the name `EWR CCCP`. + -- * `"SQUADRON CCCP"`: Late activated Groups objects of the red coalition are placed above the relevant airbases that will contain these templates in the squadron. + -- These late activated Groups start with the name `SQUADRON CCCP`. Each Group object contains only one Unit, and defines the weapon payload, skin and skill level. + -- * `"CAP CCCP"`: CAP Zones are defined using floating, late activated Helicopter Group objects, where the route points define the route of the polygon of the CAP Zone. + -- These Helicopter Group objects start with the name `CAP CCCP`, and will be the locations wherein CAP will be performed. + -- * `2` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. + -- + -- + -- ### 4.2) A more advanced setup: + -- + -- -- Setup the AI_A2G_GCICAP dispatcher for the blue coalition. + -- + -- A2G_GCICAP_Blue = AI_A2G_GCICAP:New( { "BLUE EWR" }, { "104th", "105th", "106th" }, { "104th CAP" }, 4 ) + -- + -- The following parameters for the :New method have the following meaning: + -- + -- * `{ "BLUE EWR" }`: An array of the group name prefixes of the groups of the blue coalition are placed that define the EWR network. These groups start with the name `BLUE EWR`. + -- * `{ "104th", "105th", "106th" } `: An array of the group name prefixes of the Late activated Groups objects of the blue coalition are + -- placed above the relevant airbases that will contain these templates in the squadron. + -- These late activated Groups start with the name `104th` or `105th` or `106th`. + -- * `{ "104th CAP" }`: An array of the names of the CAP zones are defined using floating, late activated helicopter group objects, + -- where the route points define the route of the polygon of the CAP Zone. + -- These Helicopter Group objects start with the name `104th CAP`, and will be the locations wherein CAP will be performed. + -- * `4` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. + -- + -- @field #AI_A2G_GCICAP + AI_A2G_GCICAP = { + ClassName = "AI_A2G_GCICAP", + Detection = nil, + } + + + --- AI_A2G_GCICAP constructor. + -- @param #AI_A2G_GCICAP self + -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. + -- @param #string TemplatePrefixes A list of template prefixes. + -- @param #string PatrolPrefixes A list of CAP zone prefixes (polygon zones). + -- @param #number PatrolLimit A number of how many CAP maximum will be spawned. + -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. + -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. + -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. + -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. + -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. + -- @return #AI_A2G_GCICAP + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is DF CCCP. All groups starting with DF CCCP will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is nil. No CAP is created. + -- -- The CAP Limit is nil. + -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. + -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. + -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, nil, nil, nil, nil, nil, 30 ) + -- + function AI_A2G_GCICAP:New( EWRPrefixes, TemplatePrefixes, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + + local EWRSetGroup = SET_GROUP:New() + EWRSetGroup:FilterPrefixes( EWRPrefixes ) + EWRSetGroup:FilterStart() + + local Detection = DETECTION_AREAS:New( EWRSetGroup, GroupingRadius or 30000 ) + + local self = BASE:Inherit( self, AI_A2G_DISPATCHER:New( Detection ) ) -- #AI_A2G_GCICAP + + self:SetGciRadius( GciRadius ) + + -- Determine the coalition of the EWRNetwork, this will be the coalition of the GCICAP. + local EWRFirst = EWRSetGroup:GetFirst() -- Wrapper.Group#GROUP + local EWRCoalition = EWRFirst:GetCoalition() + + -- Determine the airbases belonging to the coalition. + local AirbaseNames = {} -- #list<#string> + for AirbaseID, AirbaseData in pairs( _DATABASE.AIRBASES ) do + local Airbase = AirbaseData -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + if Airbase:GetCoalition() == EWRCoalition then + table.insert( AirbaseNames, AirbaseName ) + end + end + + self.Templates = SET_GROUP + :New() + :FilterPrefixes( TemplatePrefixes ) + :FilterOnce() + + -- Setup squadrons + + self:I( { Airbases = AirbaseNames } ) + + self:I( "Defining Templates for Airbases ..." ) + for AirbaseID, AirbaseName in pairs( AirbaseNames ) do + local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + local AirbaseCoord = Airbase:GetCoordinate() + local AirbaseZone = ZONE_RADIUS:New( "Airbase", AirbaseCoord:GetVec2(), 3000 ) + local Templates = nil + self:I( { Airbase = AirbaseName } ) + for TemplateID, Template in pairs( self.Templates:GetSet() ) do + local Template = Template -- Wrapper.Group#GROUP + local TemplateCoord = Template:GetCoordinate() + if AirbaseZone:IsVec2InZone( TemplateCoord:GetVec2() ) then + Templates = Templates or {} + table.insert( Templates, Template:GetName() ) + self:I( { Template = Template:GetName() } ) + end + end + if Templates then + self:SetSquadron( AirbaseName, AirbaseName, Templates, ResourceCount ) + end + end + + -- Setup CAP. + -- Find for each CAP the nearest airbase to the (start or center) of the zone. + -- CAP will be launched from there. + + self.CAPTemplates = SET_GROUP:New() + self.CAPTemplates:FilterPrefixes( PatrolPrefixes ) + self.CAPTemplates:FilterOnce() + + self:I( "Setting up CAP ..." ) + for CAPID, CAPTemplate in pairs( self.CAPTemplates:GetSet() ) do + local CAPZone = ZONE_POLYGON:New( CAPTemplate:GetName(), CAPTemplate ) + -- Now find the closest airbase from the ZONE (start or center) + local AirbaseDistance = 99999999 + local AirbaseClosest = nil -- Wrapper.Airbase#AIRBASE + self:I( { CAPZoneGroup = CAPID } ) + for AirbaseID, AirbaseName in pairs( AirbaseNames ) do + local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + local AirbaseCoord = Airbase:GetCoordinate() + local Squadron = self.DefenderSquadrons[AirbaseName] + if Squadron then + local Distance = AirbaseCoord:Get2DDistance( CAPZone:GetCoordinate() ) + self:I( { AirbaseDistance = Distance } ) + if Distance < AirbaseDistance then + AirbaseDistance = Distance + AirbaseClosest = Airbase + end + end + end + if AirbaseClosest then + self:I( { CAPAirbase = AirbaseClosest:GetName() } ) + self:SetSquadronPatrol( AirbaseClosest:GetName(), CAPZone, 6000, 10000, 500, 800, 800, 1200, "RADIO" ) + self:SetSquadronPatrolInterval( AirbaseClosest:GetName(), PatrolLimit, 300, 600, 1 ) + end + end + + -- Setup GCI. + -- GCI is setup for all Squadrons. + self:I( "Setting up GCI ..." ) + for AirbaseID, AirbaseName in pairs( AirbaseNames ) do + local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + local Squadron = self.DefenderSquadrons[AirbaseName] + self:F( { Airbase = AirbaseName } ) + if Squadron then + self:I( { GCIAirbase = AirbaseName } ) + self:SetSquadronGci( AirbaseName, 800, 1200 ) + end + end + + self:__Start( 5 ) + + self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) + self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) + + self:HandleEvent( EVENTS.Land ) + self:HandleEvent( EVENTS.EngineShutdown ) + + return self + end + + --- AI_A2G_GCICAP constructor with border. + -- @param #AI_A2G_GCICAP self + -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. + -- @param #string TemplatePrefixes A list of template prefixes. + -- @param #string BorderPrefix A Border Zone Prefix. + -- @param #string PatrolPrefixes A list of CAP zone prefixes (polygon zones). + -- @param #number PatrolLimit A number of how many CAP maximum will be spawned. + -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. + -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. + -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. + -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. + -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. + -- @return #AI_A2G_GCICAP + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is nil. No CAP is created. + -- -- The CAP Limit is nil. + -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. + -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. + -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", nil, nil, nil, nil, nil, 30 ) + -- + function AI_A2G_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + + local self = AI_A2G_GCICAP:New( EWRPrefixes, TemplatePrefixes, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + + if BorderPrefix then + self:SetBorderZone( ZONE_POLYGON:New( BorderPrefix, GROUP:FindByName( BorderPrefix ) ) ) + end + + return self + + end + +end + diff --git a/Moose Development/Moose/AI/AI_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua new file mode 100644 index 000000000..2cc6845b1 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_Engage.lua @@ -0,0 +1,440 @@ +--- **AI** -- Models the process of air to ground engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_Engage +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_ENGAGE +-- @extends AI.AI_A2A#AI_A2A + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- ![Process](..\Presentations\AI_GCI\Dia3.JPG) +-- +-- The AI_A2G_ENGAGE is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_ENGAGE process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_GCI\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_GCI\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_GCI\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_GCI\Dia13.JPG) +-- +-- ## 1. AI_A2G_ENGAGE constructor +-- +-- * @{#AI_A2G_ENGAGE.New}(): Creates a new AI_A2G_ENGAGE object. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_GCI\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_GCI#AI_A2G_ENGAGE.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_A2G_ENGAGE.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2G_ENGAGE +AI_A2G_ENGAGE = { + ClassName = "AI_A2G_ENGAGE", +} + + + +--- Creates a new AI_A2G_ENGAGE object +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup +-- @return #AI_A2G_ENGAGE +function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G:New( AIGroup ) ) -- #AI_A2G_ENGAGE + + self.Accomplished = false + self.Engaging = false + + self.EngageMinSpeed = EngageMinSpeed + self.EngageMaxSpeed = EngageMaxSpeed + self.PatrolMinSpeed = EngageMinSpeed + self.PatrolMaxSpeed = EngageMaxSpeed + + self.PatrolAltType = "RADIO" + + self:AddTransition( { "Started", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeEngage + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterEngage + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] Engage + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] __Engage + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_A2G_ENGAGE] OnLeaveEngaging +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_A2G_ENGAGE] OnEnterEngaging +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeFired + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterFired + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] Fired + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] __Fired + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeDestroy + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterDestroy + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] Destroy + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] __Destroy + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeAbort + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterAbort + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] Abort + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] __Abort + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeAccomplish + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterAccomplish + -- @param #AI_A2G_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] Accomplish + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] __Accomplish + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + return self +end + +--- onafter event handler for Start event. +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterStart( AIGroup, From, Event, To ) + + self:GetParent( self ).onafterStart( self, AIGroup, From, Event, To ) + AIGroup:HandleEvent( EVENTS.Takeoff, nil, self ) + +end + + + +--- onafter event handler for Engage event. +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterEngage( AIGroup, From, Event, To ) + + self:HandleEvent( EVENTS.Dead ) + +end + +-- todo: need to fix this global function + +--- @param Wrapper.Group#GROUP AIControllable +function AI_A2G_ENGAGE.EngageRoute( AIGroup, Fsm ) + + AIGroup:F( { "AI_A2G_ENGAGE.EngageRoute:", AIGroup:GetName() } ) + + if AIGroup:IsAlive() then + Fsm:__Engage( 0.5 ) + + --local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) + --AIGroup:SetTask( Task ) + end +end + +--- onbefore event handler for Engage event. +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onbeforeEngage( AIGroup, From, Event, To ) + + if self.Accomplished == true then + return false + end +end + +--- onafter event handler for Abort event. +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterAbort( AIGroup, From, Event, To ) + AIGroup:ClearTasks() + self:Return() + self:__RTB( 0.5 ) +end + + +--- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The GroupGroup managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterEngage( AIGroup, From, Event, To, AttackSetUnit ) + + self:F( { AIGroup, From, Event, To, AttackSetUnit} ) + + self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + + local FirstAttackUnit = self.AttackSetUnit:GetFirst() + + if FirstAttackUnit and FirstAttackUnit:IsAlive() then + + if AIGroup:IsAlive() then + + local EngageRoute = {} + + local CurrentCoord = AIGroup:GetCoordinate() + + --- Calculate the target route point. + + local CurrentCoord = AIGroup:GetCoordinate() + + local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + self:SetTargetDistance( ToTargetCoord ) -- For RTB status check + + local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + local ToEngageAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) + + --- Create a route point of type air. + local ToPatrolRoutePoint = CurrentCoord:Translate( 15000, ToEngageAngle ):WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = ToEngageAngle, ToTargetSpeed = ToTargetSpeed } ) + self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint + EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint + + local AttackTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "Eliminating Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) + AttackTasks[#AttackTasks+1] = AIGroup:TaskAttackUnit( AttackUnit ) + end + end + + if #AttackTasks == 0 then + self:E("No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + else + AIGroup:OptionROEOpenFire() + AIGroup:OptionROTEvadeFire() + + AttackTasks[#AttackTasks+1] = AIGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( AttackTasks ) + end + + AIGroup:Route( EngageRoute, 0.5 ) + + end + else + self:E("No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + end +end + +--- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_ENGAGE:onafterAccomplish( AIGroup, From, Event, To ) + self.Accomplished = true + self:SetDetectionOff() +end + +--- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_A2G_ENGAGE:onafterDestroy( AIGroup, From, Event, To, EventData ) + + if EventData.IniUnit then + self.AttackUnits[EventData.IniUnit] = nil + end +end + +--- @param #AI_A2G_ENGAGE self +-- @param Core.Event#EVENTDATA EventData +function AI_A2G_ENGAGE:OnEventDead( EventData ) + self:F( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then + self:__Destroy( 1, EventData ) + end + end +end diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua new file mode 100644 index 000000000..bf4a97dba --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_Patrol.lua @@ -0,0 +1,488 @@ +--- **AI** -- Models the process of A2G patrolling and engaging ground targets for airplanes and helicopters. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_Patrol +-- @image AI_Air_To_Ground_Patrol.JPG + +--- @type AI_A2G_PATROL +-- @extends AI.AI_A2A_Patrol#AI_A2A_PATROL + + +--- The AI_A2G_PATROL class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} +-- and automatically engage any airborne enemies that are within a certain range or within a certain zone. +-- +-- ![Process](..\Presentations\AI_CAP\Dia3.JPG) +-- +-- The AI_A2G_PATROL is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_PATROL process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_CAP\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_CAP\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_CAP\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_CAP\Dia13.JPG) +-- +-- ## 1. AI_A2G_PATROL constructor +-- +-- * @{#AI_A2G_PATROL.New}(): Creates a new AI_A2G_PATROL object. +-- +-- ## 2. AI_A2G_PATROL is a FSM +-- +-- ![Process](..\Presentations\AI_CAP\Dia2.JPG) +-- +-- ### 2.1 AI_A2G_PATROL States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the bogeys. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 2.2 AI_A2G_PATROL Events +-- +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{#AI_A2G_PATROL.Engage}**: Let the AI engage the bogeys. +-- * **@{#AI_A2G_PATROL.Abort}**: Aborts the engagement and return patrolling in the patrol zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_A2G_PATROL.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. +-- * **@{#AI_A2G_PATROL.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_CAP\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_CAP#AI_A2G_PATROL.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_A2G_PATROL.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2G_PATROL +AI_A2G_PATROL = { + ClassName = "AI_A2G_PATROL", +} + +--- Creates a new AI_A2G_PATROL object +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_PATROL +function AI_A2G_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2A_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_PATROL + + self.Accomplished = false + self.Engaging = false + + self.EngageMinSpeed = EngageMinSpeed + self.EngageMaxSpeed = EngageMaxSpeed + + self:AddTransition( { "Patrolling", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_A2G_PATROL] OnBeforeEngage + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_A2G_PATROL] OnAfterEngage + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_A2G_PATROL] Engage + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_A2G_PATROL] __Engage + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_A2G_PATROL] OnLeaveEngaging +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_A2G_PATROL] OnEnterEngaging +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_A2G_PATROL] OnBeforeFired + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_A2G_PATROL] OnAfterFired + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_A2G_PATROL] Fired + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_A2G_PATROL] __Fired + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_A2G_PATROL] OnBeforeDestroy + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_A2G_PATROL] OnAfterDestroy + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_A2G_PATROL] Destroy + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_A2G_PATROL] __Destroy + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_A2G_PATROL] OnBeforeAbort + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_A2G_PATROL] OnAfterAbort + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_A2G_PATROL] Abort + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_A2G_PATROL] __Abort + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_A2G_PATROL] OnBeforeAccomplish + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_A2G_PATROL] OnAfterAccomplish + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_A2G_PATROL] Accomplish + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_A2G_PATROL] __Accomplish + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + + return self +end + + +--- onafter State Transition for Event Patrol. +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onafterStart( AIPatrol, From, Event, To ) + + self:GetParent( self ).onafterStart( self, AIPatrol, From, Event, To ) + AIPatrol:HandleEvent( EVENTS.Takeoff, nil, self ) + +end + +--- Set the Engage Zone which defines where the AI will engage bogies. +-- @param #AI_A2G_PATROL self +-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. +-- @return #AI_A2G_PATROL self +function AI_A2G_PATROL:SetEngageZone( EngageZone ) + self:F2() + + if EngageZone then + self.EngageZone = EngageZone + else + self.EngageZone = nil + end +end + +--- Set the Engage Range when the AI will engage with airborne enemies. +-- @param #AI_A2G_PATROL self +-- @param #number EngageRange The Engage Range. +-- @return #AI_A2G_PATROL self +function AI_A2G_PATROL:SetEngageRange( EngageRange ) + self:F2() + + if EngageRange then + self.EngageRange = EngageRange + else + self.EngageRange = nil + end +end + +--- onafter State Transition for Event Patrol. +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onafterPatrol( AIPatrol, From, Event, To ) + + -- Call the parent Start event handler + self:GetParent(self).onafterPatrol( self, AIPatrol, From, Event, To ) + self:HandleEvent( EVENTS.Dead ) + +end + +-- todo: need to fix this global function + +--- @param Wrapper.Group#GROUP AIPatrol +function AI_A2G_PATROL.AttackRoute( AIPatrol, Fsm ) + + AIPatrol:F( { "AI_A2G_PATROL.AttackRoute:", AIPatrol:GetName() } ) + + if AIPatrol:IsAlive() then + Fsm:__Engage( 0.5 ) + end +end + +--- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onbeforeEngage( AIPatrol, From, Event, To ) + + if self.Accomplished == true then + return false + end +end + +--- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onafterAbort( AIPatrol, From, Event, To ) + AIPatrol:ClearTasks() + self:__Route( 0.5 ) +end + + +--- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The AIPatrol Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onafterEngage( AIPatrol, From, Event, To, AttackSetUnit ) + + self:F( { AIPatrol, From, Event, To, AttackSetUnit} ) + + self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + + local FirstAttackUnit = self.AttackSetUnit:GetFirst() -- Wrapper.Unit#UNIT + + if FirstAttackUnit and FirstAttackUnit:IsAlive() then -- If there is no attacker anymore, stop the engagement. + + if AIPatrol:IsAlive() then + + local EngageRoute = {} + + --- Calculate the target route point. + local CurrentCoord = AIPatrol:GetCoordinate() + local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + local ToInterceptAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) + + --- Create a route point of type air. + local ToPatrolRoutePoint = CurrentCoord:Translate( 5000, ToInterceptAngle ):WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = ToInterceptAngle, ToTargetSpeed = ToTargetSpeed } ) + self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint + EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint + + local AttackTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT + self:T( { "Attacking Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + AttackTasks[#AttackTasks+1] = AIPatrol:TaskAttackUnit( AttackUnit ) + end + end + + if #AttackTasks == 0 then + self:E("No targets found -> Going back to Patrolling") + self:__Abort( 0.5 ) + else + AIPatrol:OptionROEOpenFire() + AIPatrol:OptionROTEvadeFire() + + AttackTasks[#AttackTasks+1] = AIPatrol:TaskFunction( "AI_A2G_PATROL.AttackRoute", self ) + EngageRoute[#EngageRoute].task = AIPatrol:TaskCombo( AttackTasks ) + end + + AIPatrol:Route( EngageRoute, 0.5 ) + end + else + self:E("No targets found -> Going back to Patrolling") + self:__Abort( 0.5 ) + end +end + +--- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_PATROL:onafterAccomplish( AIPatrol, From, Event, To ) + self.Accomplished = true + self:SetDetectionOff() +end + +--- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_A2G_PATROL:onafterDestroy( AIPatrol, From, Event, To, EventData ) + + if EventData.IniUnit then + self.AttackUnits[EventData.IniUnit] = nil + end +end + +--- @param #AI_A2G_PATROL self +-- @param Core.Event#EVENTDATA EventData +function AI_A2G_PATROL:OnEventDead( EventData ) + self:F( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then + self:__Destroy( 1, EventData ) + end + end +end + +--- @param Wrapper.Group#GROUP AIPatrol +function AI_A2G_PATROL.Resume( AIPatrol, Fsm ) + + AIPatrol:I( { "AI_A2G_PATROL.Resume:", AIPatrol:GetName() } ) + if AIPatrol:IsAlive() then + Fsm:__Reset( 1 ) + Fsm:__Route( 5 ) + end + +end diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua new file mode 100644 index 000000000..80a58bf7f --- /dev/null +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -0,0 +1,732 @@ +--- **AI** -- Models the process of AI air operations. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Air +-- @image AI_Air_Operations.JPG + +--- @type AI_AIR +-- @extends Core.Fsm#FSM_CONTROLLABLE + +--- The AI_AIR class implements the core functions to operate an AI @{Wrapper.Group}. +-- +-- +-- # 1) AI_AIR constructor +-- +-- * @{#AI_AIR.New}(): Creates a new AI_AIR object. +-- +-- # 2) AI_AIR is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- +-- ## 2.1) AI_AIR States. +-- +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_AIR Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- @field #AI_AIR +AI_AIR = { + ClassName = "AI_AIR", +} + +--- Creates a new AI_AIR process. +-- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process. +-- @return #AI_AIR +function AI_AIR:New( AIGroup ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_AIR + + self:SetControllable( AIGroup ) + + self:SetStartState( "Stopped" ) + + self:AddTransition( "*", "Start", "Started" ) + + --- Start Handler OnBefore for AI_AIR + -- @function [parent=#AI_AIR] OnBeforeStart + -- @param #AI_AIR self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Start Handler OnAfter for AI_AIR + -- @function [parent=#AI_AIR] OnAfterStart + -- @param #AI_AIR self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Start Trigger for AI_AIR + -- @function [parent=#AI_AIR] Start + -- @param #AI_AIR self + + --- Start Asynchronous Trigger for AI_AIR + -- @function [parent=#AI_AIR] __Start + -- @param #AI_AIR self + -- @param #number Delay + + self:AddTransition( "*", "Stop", "Stopped" ) + +--- OnLeave Transition Handler for State Stopped. +-- @function [parent=#AI_AIR] OnLeaveStopped +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Stopped. +-- @function [parent=#AI_AIR] OnEnterStopped +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- OnBefore Transition Handler for Event Stop. +-- @function [parent=#AI_AIR] OnBeforeStop +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Stop. +-- @function [parent=#AI_AIR] OnAfterStop +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Stop. +-- @function [parent=#AI_AIR] Stop +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event Stop. +-- @function [parent=#AI_AIR] __Stop +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. + +--- OnBefore Transition Handler for Event Status. +-- @function [parent=#AI_AIR] OnBeforeStatus +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Status. +-- @function [parent=#AI_AIR] OnAfterStatus +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Status. +-- @function [parent=#AI_AIR] Status +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event Status. +-- @function [parent=#AI_AIR] __Status +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "RTB", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. + +--- OnBefore Transition Handler for Event RTB. +-- @function [parent=#AI_AIR] OnBeforeRTB +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event RTB. +-- @function [parent=#AI_AIR] OnAfterRTB +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event RTB. +-- @function [parent=#AI_AIR] RTB +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event RTB. +-- @function [parent=#AI_AIR] __RTB +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Returning. +-- @function [parent=#AI_AIR] OnLeaveReturning +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Returning. +-- @function [parent=#AI_AIR] OnEnterReturning +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Patrolling", "Refuel", "Refuelling" ) + + --- Refuel Handler OnBefore for AI_AIR + -- @function [parent=#AI_AIR] OnBeforeRefuel + -- @param #AI_AIR self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Refuel Handler OnAfter for AI_AIR + -- @function [parent=#AI_AIR] OnAfterRefuel + -- @param #AI_AIR self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Refuel Trigger for AI_AIR + -- @function [parent=#AI_AIR] Refuel + -- @param #AI_AIR self + + --- Refuel Asynchronous Trigger for AI_AIR + -- @function [parent=#AI_AIR] __Refuel + -- @param #AI_AIR self + -- @param #number Delay + + self:AddTransition( "*", "Takeoff", "Airborne" ) + self:AddTransition( "*", "Return", "Returning" ) + self:AddTransition( "*", "Hold", "Holding" ) + self:AddTransition( "*", "Home", "Home" ) + self:AddTransition( "*", "LostControl", "LostControl" ) + self:AddTransition( "*", "Fuel", "Fuel" ) + self:AddTransition( "*", "Damaged", "Damaged" ) + self:AddTransition( "*", "Eject", "*" ) + self:AddTransition( "*", "Crash", "Crashed" ) + self:AddTransition( "*", "PilotDead", "*" ) + + self.IdleCount = 0 + + return self +end + +--- @param Wrapper.Group#GROUP self +-- @param Core.Event#EVENTDATA EventData +function GROUP:OnEventTakeoff( EventData, Fsm ) + Fsm:Takeoff() + self:UnHandleEvent( EVENTS.Takeoff ) +end + + + +function AI_AIR:SetDispatcher( Dispatcher ) + self.Dispatcher = Dispatcher +end + +function AI_AIR:GetDispatcher() + return self.Dispatcher +end + +function AI_AIR:SetTargetDistance( Coordinate ) + + local CurrentCoord = self.Controllable:GetCoordinate() + self.TargetDistance = CurrentCoord:Get2DDistance( Coordinate ) + + self.ClosestTargetDistance = ( not self.ClosestTargetDistance or self.ClosestTargetDistance > self.TargetDistance ) and self.TargetDistance or self.ClosestTargetDistance +end + + +function AI_AIR:ClearTargetDistance() + + self.TargetDistance = nil + self.ClosestTargetDistance = nil +end + + +--- Sets (modifies) the minimum and maximum speed of the patrol. +-- @param #AI_AIR self +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @return #AI_AIR self +function AI_AIR:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) + self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) + + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed +end + + +--- Sets the floor and ceiling altitude of the patrol. +-- @param #AI_AIR self +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @return #AI_AIR self +function AI_AIR:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) + self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) + + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude +end + + +--- Sets the home airbase. +-- @param #AI_AIR self +-- @param Wrapper.Airbase#AIRBASE HomeAirbase +-- @return #AI_AIR self +function AI_AIR:SetHomeAirbase( HomeAirbase ) + self:F2( { HomeAirbase } ) + + self.HomeAirbase = HomeAirbase +end + +--- Sets to refuel at the given tanker. +-- @param #AI_AIR self +-- @param Wrapper.Group#GROUP TankerName The group name of the tanker as defined within the Mission Editor or spawned. +-- @return #AI_AIR self +function AI_AIR:SetTanker( TankerName ) + self:F2( { TankerName } ) + + self.TankerName = TankerName +end + + +--- Sets the disengage range, that when engaging a target beyond the specified range, the engagement will be cancelled and the plane will RTB. +-- @param #AI_AIR self +-- @param #number DisengageRadius The disengage range. +-- @return #AI_AIR self +function AI_AIR:SetDisengageRadius( DisengageRadius ) + self:F2( { DisengageRadius } ) + + self.DisengageRadius = DisengageRadius +end + +--- Set the status checking off. +-- @param #AI_AIR self +-- @return #AI_AIR self +function AI_AIR:SetStatusOff() + self:F2() + + self.CheckStatus = false +end + + +--- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. +-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. +-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_AIR. +-- Once the time is finished, the old AI will return to the base. +-- @param #AI_AIR self +-- @param #number FuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. +-- @param #number OutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. +-- @return #AI_AIR self +function AI_AIR:SetFuelThreshold( FuelThresholdPercentage, OutOfFuelOrbitTime ) + + self.FuelThresholdPercentage = FuelThresholdPercentage + self.OutOfFuelOrbitTime = OutOfFuelOrbitTime + + self.Controllable:OptionRTBBingoFuel( false ) + + return self +end + +--- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. +-- However, damage cannot be foreseen early on. +-- Therefore, when the damage treshold is reached, +-- the AI will return immediately to the home base (RTB). +-- Note that for groups, the average damage of the complete group will be calculated. +-- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. +-- @param #AI_AIR self +-- @param #number PatrolDamageThreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. +-- @return #AI_AIR self +function AI_AIR:SetDamageThreshold( PatrolDamageThreshold ) + + self.PatrolManageDamage = true + self.PatrolDamageThreshold = PatrolDamageThreshold + + return self +end + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_AIR self +-- @return #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR:onafterStart( Controllable, From, Event, To ) + + self:__Status( 10 ) -- Check status status every 30 seconds. + + self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) + self:HandleEvent( EVENTS.Crash, self.OnCrash ) + self:HandleEvent( EVENTS.Ejection, self.OnEjection ) + + Controllable:OptionROEHoldFire() + Controllable:OptionROTVertical() +end + + + +--- @param #AI_AIR self +function AI_AIR:onbeforeStatus() + + return self.CheckStatus +end + +--- @param #AI_AIR self +function AI_AIR:onafterStatus() + + if self.Controllable and self.Controllable:IsAlive() then + + local RTB = false + + local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) + + if not self:Is( "Holding" ) and not self:Is( "Returning" ) then + local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) + self:F({DistanceFromHomeBase=DistanceFromHomeBase}) + + if DistanceFromHomeBase > self.DisengageRadius then + self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) + self:Hold( 300 ) + RTB = false + end + end + +-- I think this code is not requirement anymore after release 2.5. +-- if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then +-- if DistanceFromHomeBase < 5000 then +-- self:E( self.Controllable:GetName() .. " is near the home base, RTB!" ) +-- self:Home( "Destroy" ) +-- end +-- end + + + if not self:Is( "Fuel" ) and not self:Is( "Home" ) then + local Fuel = self.Controllable:GetFuelMin() + self:F({Fuel=Fuel, FuelThresholdPercentage=self.FuelThresholdPercentage}) + if Fuel < self.FuelThresholdPercentage then + if self.TankerName then + self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) + self:Refuel() + else + self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... RTB!" ) + local OldAIControllable = self.Controllable + + local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.OutOfFuelOrbitTime,nil ) ) + OldAIControllable:SetTask( TimedOrbitTask, 10 ) + + self:Fuel() + RTB = true + end + else + end + end + + -- TODO: Check GROUP damage function. + local Damage = self.Controllable:GetLife() + local InitialLife = self.Controllable:GetLife0() + self:F( { Damage = Damage, InitialLife = InitialLife, DamageThreshold = self.PatrolDamageThreshold } ) + if ( Damage / InitialLife ) < self.PatrolDamageThreshold then + self:E( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) + self:Damaged() + RTB = true + self:SetStatusOff() + end + + -- Check if planes went RTB and are out of control. + -- We only check if planes are out of control, when they are in duty. + if self.Controllable:HasTask() == false then + if not self:Is( "Started" ) and + not self:Is( "Stopped" ) and + not self:Is( "Fuel" ) and + not self:Is( "Damaged" ) and + not self:Is( "Home" ) then + if self.IdleCount >= 2 then + if Damage ~= InitialLife then + self:Damaged() + else + self:E( self.Controllable:GetName() .. " control lost! " ) + self:LostControl() + end + else + self.IdleCount = self.IdleCount + 1 + end + end + else + self.IdleCount = 0 + end + + if RTB == true then + self:__RTB( 0.5 ) + end + + if not self:Is("Home") then + self:__Status( 10 ) + end + + end +end + + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.RTBRoute( AIGroup, Fsm ) + + AIGroup:F( { "AI_AIR.RTBRoute:", AIGroup:GetName() } ) + + if AIGroup:IsAlive() then + Fsm:__RTB( 0.5 ) + end + +end + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.RTBHold( AIGroup, Fsm ) + + AIGroup:F( { "AI_AIR.RTBHold:", AIGroup:GetName() } ) + if AIGroup:IsAlive() then + Fsm:__RTB( 0.5 ) + Fsm:Return() + local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) + AIGroup:SetTask( Task ) + end + +end + + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterRTB( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + + if AIGroup and AIGroup:IsAlive() then + + self:E( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) + + self:ClearTargetDistance() + AIGroup:ClearTasks() + + local EngageRoute = {} + + --- Calculate the target route point. + + local CurrentCoord = AIGroup:GetCoordinate() + local ToTargetCoord = self.HomeAirbase:GetCoordinate() + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + local ToAirbaseAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) + + local Distance = CurrentCoord:Get2DDistance( ToTargetCoord ) + + local ToAirbaseCoord = CurrentCoord:Translate( 5000, ToAirbaseAngle ) + if Distance < 5000 then + self:E( "RTB and near the airbase!" ) + self:Home() + return + end + --- Create a route point of type air. + local ToRTBRoutePoint = ToAirbaseCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = ToAirbaseAngle, ToTargetSpeed = ToTargetSpeed } ) + self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToRTBRoutePoint + EngageRoute[#EngageRoute+1] = ToRTBRoutePoint + + AIGroup:OptionROEHoldFire() + AIGroup:OptionROTEvadeFire() + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + AIGroup:WayPointInitialize( EngageRoute ) + + local Tasks = {} + Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_AIR.RTBRoute", self ) + EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) + + --- NOW ROUTE THE GROUP! + AIGroup:Route( EngageRoute, 0.5 ) + + end + +end + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterHome( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + self:E( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + end + +end + + + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterHold( AIGroup, From, Event, To, HoldTime ) + self:F( { AIGroup, From, Event, To } ) + + self:E( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + local OrbitTask = AIGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local TimedOrbitTask = AIGroup:TaskControlled( OrbitTask, AIGroup:TaskCondition( nil, nil, nil, nil, HoldTime , nil ) ) + + local RTBTask = AIGroup:TaskFunction( "AI_AIR.RTBHold", self ) + + local OrbitHoldTask = AIGroup:TaskOrbitCircle( 4000, self.PatrolMinSpeed ) + + --AIGroup:SetState( AIGroup, "AI_AIR", self ) + + AIGroup:SetTask( AIGroup:TaskCombo( { TimedOrbitTask, RTBTask, OrbitHoldTask } ), 1 ) + end + +end + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.Resume( AIGroup, Fsm ) + + AIGroup:I( { "AI_AIR.Resume:", AIGroup:GetName() } ) + if AIGroup:IsAlive() then + Fsm:__RTB( 0.5 ) + end + +end + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + self:E( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + local Tanker = GROUP:FindByName( self.TankerName ) + if Tanker:IsAlive() and Tanker:IsAirPlane() then + + local RefuelRoute = {} + + --- Calculate the target route point. + + local CurrentCoord = AIGroup:GetCoordinate() + local ToRefuelCoord = Tanker:GetCoordinate() + local ToRefuelSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + + --- Create a route point of type air. + local ToRefuelRoutePoint = ToRefuelCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToRefuelSpeed, + true + ) + + self:F( { ToRefuelSpeed = ToRefuelSpeed } ) + + RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint + RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint + + AIGroup:OptionROEHoldFire() + AIGroup:OptionROTEvadeFire() + + local Tasks = {} + Tasks[#Tasks+1] = AIGroup:TaskRefueling() + Tasks[#Tasks+1] = AIGroup:TaskFunction( self:GetClassName() .. ".Resume", self ) + RefuelRoute[#RefuelRoute].task = AIGroup:TaskCombo( Tasks ) + + AIGroup:Route( RefuelRoute, 0.5 ) + else + self:RTB() + end + end + +end + + + +--- @param #AI_AIR self +function AI_AIR:onafterDead() + self:SetStatusOff() +end + + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnCrash( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:E( self.Controllable:GetUnits() ) + if #self.Controllable:GetUnits() == 1 then + self:__Crash( 1, EventData ) + end + end +end + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnEjection( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__Eject( 1, EventData ) + end +end + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnPilotDead( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__PilotDead( 1, EventData ) + end +end diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 105e03141..6acb294ab 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -1,24 +1,24 @@ --- **Core** - Define collections of objects to perform bulk actions and logically group objects. --- +-- -- === --- +-- -- ## Features: --- +-- -- * Dynamically maintain collections of objects. -- * Manually modify the collection, by adding or removing objects. -- * Collections of different types. -- * Validate the presence of objects in the collection. -- * Perform bulk actions on collection. --- +-- -- === --- +-- -- Group objects or data of the same type into a collection, which is either: --- +-- -- * Manually managed using the **:Add...()** or **:Remove...()** methods. The initial SET can be filtered with the **@{#SET_BASE.FilterOnce}()** method. -- * Dynamically updated when new objects are created or objects are destroyed using the **@{#SET_BASE.FilterStart}()** method. --- +-- -- Various types of SET_ classes are available: --- +-- -- * @{#SET_GROUP}: Defines a collection of @{Wrapper.Group}s filtered by filter criteria. -- * @{#SET_UNIT}: Defines a colleciton of @{Wrapper.Unit}s filtered by filter criteria. -- * @{#SET_STATIC}: Defines a collection of @{Wrapper.Static}s filtered by filter criteria. @@ -26,21 +26,21 @@ -- * @{#SET_AIRBASE}: Defines a collection of @{Wrapper.Airbase}s filtered by filter criteria. -- * @{#SET_CARGO}: Defines a collection of @{Cargo.Cargo}s filtered by filter criteria. -- * @{#SET_ZONE}: Defines a collection of @{Core.Zone}s filtered by filter criteria. --- +-- -- These classes are derived from @{#SET_BASE}, which contains the main methods to manage the collections. --- +-- -- A multitude of other methods are available in the individual set classes that allow to: --- +-- -- * Validate the presence of objects in the SET. -- * Trigger events when objects in the SET change a zone presence. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- ### Contributions: --- +-- ### Contributions: +-- -- === --- +-- -- @module Core.Set -- @image Core_Sets.JPG @@ -53,24 +53,24 @@ do -- SET_BASE -- @field #table List -- @field Core.Scheduler#SCHEDULER CallScheduler -- @extends Core.Base#BASE - - + + --- The @{Core.Set#SET_BASE} class defines the core functions that define a collection of objects. -- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach interator loop at defined **"intervals"** to the mail simulator loop. -- In this way, large loops can be done while not blocking the simulator main processing loop. -- The default **"yield interval"** is after 10 objects processed. -- The default **"time interval"** is after 0.001 seconds. - -- + -- -- ## Add or remove objects from the SET - -- + -- -- Some key core functions are @{Core.Set#SET_BASE.Add} and @{Core.Set#SET_BASE.Remove} to add or remove objects from the SET in your logic. - -- + -- -- ## Define the SET iterator **"yield interval"** and the **"time interval"** - -- + -- -- Modify the iterator intervals with the @{Core.Set#SET_BASE.SetInteratorIntervals} method. -- You can set the **"yield interval"**, and the **"time interval"**. (See above). - -- - -- @field #SET_BASE SET_BASE + -- + -- @field #SET_BASE SET_BASE SET_BASE = { ClassName = "SET_BASE", Filter = {}, @@ -78,8 +78,8 @@ do -- SET_BASE List = {}, Index = {}, } - - + + --- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_BASE self -- @return #SET_BASE @@ -87,14 +87,14 @@ do -- SET_BASE -- -- Define a new SET_BASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. -- DBObject = SET_BASE:New() function SET_BASE:New( Database ) - + -- Inherits from BASE local self = BASE:Inherit( self, FSM:New() ) -- Core.Set#SET_BASE - + self.Database = Database - + self:SetStartState( "Started" ) - + --- Added Handler OnAfter for SET_BASE -- @function [parent=#SET_BASE] OnAfterAdded -- @param #SET_BASE self @@ -103,10 +103,10 @@ do -- SET_BASE -- @param #string To -- @param #string ObjectName The name of the object. -- @param Object The object. - - + + self:AddTransition( "*", "Added", "*" ) - + --- Removed Handler OnAfter for SET_BASE -- @function [parent=#SET_BASE] OnAfterRemoved -- @param #SET_BASE self @@ -115,84 +115,84 @@ do -- SET_BASE -- @param #string To -- @param #string ObjectName The name of the object. -- @param Object The object. - + self:AddTransition( "*", "Removed", "*" ) - + self.YieldInterval = 10 self.TimeInterval = 0.001 - + self.Set = {} self.Index = {} - + self.CallScheduler = SCHEDULER:New( self ) - + self:SetEventPriority( 2 ) - + return self end - + --- Finds an @{Core.Base#BASE} object based on the object Name. -- @param #SET_BASE self -- @param #string ObjectName -- @return Core.Base#BASE The Object found. function SET_BASE:_Find( ObjectName ) - + local ObjectFound = self.Set[ObjectName] return ObjectFound end - - + + --- Gets the Set. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:GetSet() self:F2() - + return self.Set end - + --- Gets a list of the Names of the Objects in the Set. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:GetSetNames() -- R2.3 self:F2() - + local Names = {} - + for Name, Object in pairs( self.Set ) do table.insert( Names, Name ) end - + return Names end - - + + --- Gets a list of the Objects in the Set. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:GetSetObjects() -- R2.3 self:F2() - + local Objects = {} - + for Name, Object in pairs( self.Set ) do table.insert( Objects, Object ) end - + return Objects end - - + + --- Removes a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. -- @param #SET_BASE self -- @param #string ObjectName -- @param NoTriggerEvent (optional) When `true`, the :Remove() method will not trigger a **Removed** event. function SET_BASE:Remove( ObjectName, NoTriggerEvent ) self:F2( { ObjectName = ObjectName } ) - + local Object = self.Set[ObjectName] - - if Object then + + if Object then for Index, Key in ipairs( self.Index ) do if Key == ObjectName then table.remove( self.Index, Index ) @@ -206,8 +206,8 @@ do -- SET_BASE end end end - - + + --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using a given ObjectName as the index. -- @param #SET_BASE self -- @param #string ObjectName @@ -215,197 +215,197 @@ do -- SET_BASE -- @return Core.Base#BASE The added BASE Object. function SET_BASE:Add( ObjectName, Object ) self:F2( { ObjectName = ObjectName, Object = Object } ) - + -- Ensure that the existing element is removed from the Set before a new one is inserted to the Set if self.Set[ObjectName] then self:Remove( ObjectName, true ) end self.Set[ObjectName] = Object table.insert( self.Index, ObjectName ) - + self:Added( ObjectName, Object ) end - + --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using the Object Name as the index. -- @param #SET_BASE self -- @param Wrapper.Object#OBJECT Object -- @return Core.Base#BASE The added BASE Object. function SET_BASE:AddObject( Object ) self:F2( Object.ObjectName ) - + self:T( Object.UnitName ) self:T( Object.ObjectName ) self:Add( Object.ObjectName, Object ) - + end - - - - + + + + --- Gets a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. -- @param #SET_BASE self -- @param #string ObjectName -- @return Core.Base#BASE function SET_BASE:Get( ObjectName ) self:F( ObjectName ) - + local Object = self.Set[ObjectName] - + self:T3( { ObjectName, Object } ) return Object end - + --- Gets the first object from the @{Core.Set#SET_BASE} and derived classes. -- @param #SET_BASE self -- @return Core.Base#BASE function SET_BASE:GetFirst() - + local ObjectName = self.Index[1] local FirstObject = self.Set[ObjectName] self:T3( { FirstObject } ) - return FirstObject + return FirstObject end - + --- Gets the last object from the @{Core.Set#SET_BASE} and derived classes. -- @param #SET_BASE self -- @return Core.Base#BASE function SET_BASE:GetLast() - + local ObjectName = self.Index[#self.Index] local LastObject = self.Set[ObjectName] self:T3( { LastObject } ) - return LastObject + return LastObject end - + --- Gets a random object from the @{Core.Set#SET_BASE} and derived classes. -- @param #SET_BASE self -- @return Core.Base#BASE function SET_BASE:GetRandom() - + local RandomItem = self.Set[self.Index[math.random(#self.Index)]] self:T3( { RandomItem } ) return RandomItem end - - + + --- Retrieves the amount of objects in the @{Core.Set#SET_BASE} and derived classes. -- @param #SET_BASE self -- @return #number Count function SET_BASE:Count() - + return self.Index and #self.Index or 0 end - - + + --- Copies the Filter criteria from a given Set (for rebuilding a new Set based on an existing Set). -- @param #SET_BASE self -- @param #SET_BASE BaseSet -- @return #SET_BASE function SET_BASE:SetDatabase( BaseSet ) - + -- Copy the filter criteria of the BaseSet local OtherFilter = routines.utils.deepCopy( BaseSet.Filter ) self.Filter = OtherFilter - + -- Now base the new Set on the BaseSet self.Database = BaseSet:GetSet() return self end - - - + + + --- Define the SET iterator **"yield interval"** and the **"time interval"**. -- @param #SET_BASE self -- @param #number YieldInterval Sets the frequency when the iterator loop will yield after the number of objects processed. The default frequency is 10 objects processed. -- @param #number TimeInterval Sets the time in seconds when the main logic will resume the iterator loop. The default time is 0.001 seconds. -- @return #SET_BASE self function SET_BASE:SetIteratorIntervals( YieldInterval, TimeInterval ) - + self.YieldInterval = YieldInterval self.TimeInterval = TimeInterval - + return self end - - + + --- Filters for the defined collection. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:FilterOnce() - + for ObjectName, Object in pairs( self.Database ) do - + if self:IsIncludeObject( Object ) then self:Add( ObjectName, Object ) end end - + return self end - + --- Starts the filtering for the defined collection. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:_FilterStart() - + for ObjectName, Object in pairs( self.Database ) do - + if self:IsIncludeObject( Object ) then self:E( { "Adding Object:", ObjectName } ) self:Add( ObjectName, Object ) end end - + -- Follow alive players and clients --self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) --self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) - - + + return self end - + --- Starts the filtering of the Dead events for the collection. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:FilterDeads() --R2.1 allow deads to be filtered to automatically handle deads in the collection. - + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - + return self end - + --- Starts the filtering of the Crash events for the collection. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:FilterCrashes() --R2.1 allow crashes to be filtered to automatically handle crashes in the collection. - + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - + return self end - + --- Stops the filtering for the defined collection. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:FilterStop() - + self:UnHandleEvent( EVENTS.Birth ) self:UnHandleEvent( EVENTS.Dead ) self:UnHandleEvent( EVENTS.Crash ) - + return self end - + --- Iterate the SET_BASE while identifying the nearest object from a @{Core.Point#POINT_VEC2}. -- @param #SET_BASE self -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest object in the set. -- @return Core.Base#BASE The closest object. function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 ) self:F2( PointVec2 ) - + local NearestObject = nil local ClosestDistance = nil - + for ObjectID, ObjectData in pairs( self.Set ) do if NearestObject == nil then NearestObject = ObjectData @@ -418,12 +418,12 @@ do -- SET_BASE end end end - + return NearestObject end - - - + + + ----- Private method that registers all alive players in the mission. ---- @param #SET_BASE self ---- @return #SET_BASE self @@ -442,18 +442,18 @@ do -- SET_BASE -- end -- end -- end - -- + -- -- return self --end - + --- Events - + --- Handles the OnBirth event for the Set. -- @param #SET_BASE self -- @param Core.Event#EVENTDATA Event function SET_BASE:_EventOnBirth( Event ) self:F3( { Event } ) - + if Event.IniDCSUnit then local ObjectName, Object = self:AddInDatabase( Event ) self:T3( ObjectName, Object ) @@ -463,13 +463,13 @@ do -- SET_BASE end end end - + --- Handles the OnDead or OnCrash event for alive units set. -- @param #SET_BASE self -- @param Core.Event#EVENTDATA Event function SET_BASE:_EventOnDeadOrCrash( Event ) self:F( { Event } ) - + if Event.IniDCSUnit then local ObjectName, Object = self:FindInDatabase( Event ) if ObjectName then @@ -477,7 +477,7 @@ do -- SET_BASE end end end - + --- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). -- @param #SET_BASE self -- @param Core.Event#EVENTDATA Event @@ -493,7 +493,7 @@ do -- SET_BASE -- end -- end --end - + --- Handles the OnPlayerLeaveUnit event to clean the active players table. -- @param #SET_BASE self -- @param Core.Event#EVENTDATA Event @@ -519,19 +519,19 @@ do -- SET_BASE -- end -- end --end - + -- Iterators - + --- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. -- @param #SET_BASE self -- @param #function IteratorFunction The function that will be called. -- @return #SET_BASE self function SET_BASE:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments ) self:F3( arg ) - + Set = Set or self:GetSet() arg = arg or {} - + local function CoRoutine() local Count = 0 for ObjectID, ObjectData in pairs( Set ) do @@ -547,44 +547,44 @@ do -- SET_BASE Count = Count + 1 -- if Count % self.YieldInterval == 0 then -- coroutine.yield( false ) - -- end + -- end end return true end - + -- local co = coroutine.create( CoRoutine ) local co = CoRoutine - + local function Schedule() - + -- local status, res = coroutine.resume( co ) local status, res = co() self:T3( { status, res } ) - + if status == false then error( res ) end if res == false then return true -- resume next time the loop end - + return false end - + --self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) Schedule() - + return self end - - + + ----- Iterate the SET_BASE and call an interator function for each **alive** unit, providing the Unit and optional parameters. ---- @param #SET_BASE self ---- @param #function IteratorFunction The function that will be called when there is an alive unit in the SET_BASE. The function needs to accept a UNIT parameter. ---- @return #SET_BASE self --function SET_BASE:ForEachDCSUnitAlive( IteratorFunction, ... ) -- self:F3( arg ) - -- + -- -- self:ForEach( IteratorFunction, arg, self.DCSUnitsAlive ) -- -- return self @@ -596,9 +596,9 @@ do -- SET_BASE ---- @return #SET_BASE self --function SET_BASE:ForEachPlayer( IteratorFunction, ... ) -- self:F3( arg ) - -- + -- -- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) - -- + -- -- return self --end -- @@ -609,50 +609,50 @@ do -- SET_BASE ---- @return #SET_BASE self --function SET_BASE:ForEachClient( IteratorFunction, ... ) -- self:F3( arg ) - -- + -- -- self:ForEach( IteratorFunction, arg, self.Clients ) -- -- return self --end - - + + --- Decides whether to include the Object -- @param #SET_BASE self -- @param #table Object -- @return #SET_BASE self function SET_BASE:IsIncludeObject( Object ) self:F3( Object ) - + return true end - + --- Gets a string with all the object names. -- @param #SET_BASE self -- @return #string A string with the names of the objects. function SET_BASE:GetObjectNames() self:F3() - + local ObjectNames = "" for ObjectName, Object in pairs( self.Set ) do ObjectNames = ObjectNames .. ObjectName .. ", " end - + return ObjectNames end - + --- Flushes the current SET_BASE contents in the log ... (for debugging reasons). -- @param #SET_BASE self -- @param Core.Base#BASE MasterObject (optional) The master object as a reference. -- @return #string A string with the names of the objects. function SET_BASE:Flush( MasterObject ) self:F3() - + local ObjectNames = "" for ObjectName, Object in pairs( self.Set ) do ObjectNames = ObjectNames .. ObjectName .. ", " end self:F( { MasterObject = MasterObject and MasterObject:GetClassNameAndID(), "Objects in Set:", ObjectNames } ) - + return ObjectNames end @@ -663,60 +663,60 @@ do -- SET_GROUP --- @type SET_GROUP -- @extends Core.Set#SET_BASE - + --- Mission designers can use the @{Core.Set#SET_GROUP} class to build sets of groups belonging to certain: - -- + -- -- * Coalitions -- * Categories -- * Countries -- * Starting with certain prefix strings. - -- + -- -- ## SET_GROUP constructor - -- + -- -- Create a new SET_GROUP object with the @{#SET_GROUP.New} method: - -- + -- -- * @{#SET_GROUP.New}: Creates a new SET_GROUP object. - -- + -- -- ## Add or Remove GROUP(s) from SET_GROUP - -- - -- GROUPS can be added and removed using the @{Core.Set#SET_GROUP.AddGroupsByName} and @{Core.Set#SET_GROUP.RemoveGroupsByName} respectively. + -- + -- GROUPS can be added and removed using the @{Core.Set#SET_GROUP.AddGroupsByName} and @{Core.Set#SET_GROUP.RemoveGroupsByName} respectively. -- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_GROUP. - -- + -- -- ## SET_GROUP filter criteria - -- + -- -- You can set filter criteria to define the set of groups within the SET_GROUP. -- Filter criteria are defined by: - -- + -- -- * @{#SET_GROUP.FilterCoalitions}: Builds the SET_GROUP with the groups belonging to the coalition(s). -- * @{#SET_GROUP.FilterCategories}: Builds the SET_GROUP with the groups belonging to the category(ies). -- * @{#SET_GROUP.FilterCountries}: Builds the SET_GROUP with the gruops belonging to the country(ies). -- * @{#SET_GROUP.FilterPrefixes}: Builds the SET_GROUP with the groups starting with the same prefix string(s). -- * @{#SET_GROUP.FilterActive}: Builds the SET_GROUP with the groups that are only active. Groups that are inactive (late activation) won't be included in the set! - -- + -- -- For the Category Filter, extra methods have been added: - -- + -- -- * @{#SET_GROUP.FilterCategoryAirplane}: Builds the SET_GROUP from airplanes. -- * @{#SET_GROUP.FilterCategoryHelicopter}: Builds the SET_GROUP from helicopters. -- * @{#SET_GROUP.FilterCategoryGround}: Builds the SET_GROUP from ground vehicles or infantry. -- * @{#SET_GROUP.FilterCategoryShip}: Builds the SET_GROUP from ships. -- * @{#SET_GROUP.FilterCategoryStructure}: Builds the SET_GROUP from structures. - -- - -- + -- + -- -- Once the filter criteria have been set for the SET_GROUP, you can start filtering using: - -- + -- -- * @{#SET_GROUP.FilterStart}: Starts the filtering of the groups within the SET_GROUP and add or remove GROUP objects **dynamically**. -- * @{#SET_GROUP.FilterOnce}: Filters of the groups **once**. - -- + -- -- Planned filter criteria within development are (so these are not yet available): - -- + -- -- * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Core.Zone#ZONE}. - -- + -- -- ## SET_GROUP iterators - -- + -- -- Once the filters have been defined and the SET_GROUP has been built, you can iterate the SET_GROUP with the available iterator methods. -- The iterator methods will walk the SET_GROUP set, and call for each element within the set a function that you provide. -- The following iterator methods are currently available within the SET_GROUP: - -- + -- -- * @{#SET_GROUP.ForEachGroup}: Calls a function for each alive group it finds within the SET_GROUP. -- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. -- * @{#SET_GROUP.ForEachGroupPartlyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. @@ -724,39 +724,39 @@ do -- SET_GROUP -- -- -- ## SET_GROUP trigger events on the GROUP objects. - -- + -- -- The SET is derived from the FSM class, which provides extra capabilities to track the contents of the GROUP objects in the SET_GROUP. - -- + -- -- ### When a GROUP object crashes or is dead, the SET_GROUP will trigger a **Dead** event. - -- - -- You can handle the event using the OnBefore and OnAfter event handlers. + -- + -- You can handle the event using the OnBefore and OnAfter event handlers. -- The event handlers need to have the paramters From, Event, To, GroupObject. -- The GroupObject is the GROUP object that is dead and within the SET_GROUP, and is passed as a parameter to the event handler. -- See the following example: - -- + -- -- -- Create the SetCarrier SET_GROUP collection. -- -- local SetHelicopter = SET_GROUP:New():FilterPrefixes( "Helicopter" ):FilterStart() - -- + -- -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset. -- -- function SetHelicopter:OnAfterDead( From, Event, To, GroupObject ) -- self:F( { GroupObject = GroupObject:GetName() } ) -- end - -- + -- -- While this is a good example, there is a catch. -- Imageine you want to execute the code above, the the self would need to be from the object declared outside (above) the OnAfterDead method. -- So, the self would need to contain another object. Fortunately, this can be done, but you must use then the **`.`** notation for the method. -- See the modified example: - -- + -- -- -- Now we have a constructor of the class AI_CARGO_DISPATCHER, that receives the SetHelicopter as a parameter. -- -- Within that constructor, we want to set an enclosed event handler OnAfterDead for SetHelicopter. -- -- But within the OnAfterDead method, we want to refer to the self variable of the AI_CARGO_DISPATCHER. - -- + -- -- function AI_CARGO_DISPATCHER:New( SetCarrier, SetCargo, SetDeployZones ) - -- + -- -- local self = BASE:Inherit( self, FSM:New() ) -- #AI_CARGO_DISPATCHER - -- + -- -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset. -- -- Note the "." notation, and the explicit declaration of SetHelicopter, which would be using the ":" notation the implicit self variable declaration. -- @@ -765,11 +765,11 @@ do -- SET_GROUP -- self.PickupCargo[GroupObject] = nil -- So here I clear the PickupCargo table entry of the self object AI_CARGO_DISPATCHER. -- self.CarrierHome[GroupObject] = nil -- end - -- + -- -- end - -- + -- -- === - -- @field #SET_GROUP SET_GROUP + -- @field #SET_GROUP SET_GROUP SET_GROUP = { ClassName = "SET_GROUP", Filter = { @@ -793,8 +793,8 @@ do -- SET_GROUP }, }, } - - + + --- Creates a new SET_GROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_GROUP self -- @return #SET_GROUP @@ -802,23 +802,23 @@ do -- SET_GROUP -- -- Define a new SET_GROUP Object. This DBObject will contain a reference to all alive GROUPS. -- DBObject = SET_GROUP:New() function SET_GROUP:New() - + -- Inherits from BASE local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.GROUPS ) ) -- #SET_GROUP - + self:FilterActive( false ) - + return self end - + --- Gets the Set. -- @param #SET_GROUP self -- @return #SET_GROUP self function SET_GROUP:GetAliveSet() self:F2() - + local AliveSet = SET_GROUP:New() - + -- Clean the Set before returning with only the alive Groups. for GroupName, GroupObject in pairs( self.Set ) do local GroupObject=GroupObject --Wrapper.Group#GROUP @@ -828,83 +828,83 @@ do -- SET_GROUP end end end - + return AliveSet.Set or {} end - + --- Add a GROUP to SET_GROUP. -- Note that for each unit in the group that is set, a default cargo bay limit is initialized. -- @param Core.Set#SET_GROUP self -- @param Wrapper.Group#GROUP group The group which should be added to the set. -- @return self function SET_GROUP:AddGroup( group ) - + self:Add( group:GetName(), group ) - + -- I set the default cargo bay weight limit each time a new group is added to the set. for UnitID, UnitData in pairs( group:GetUnits() ) do UnitData:SetCargoBayWeightLimit() end - + return self end - + --- Add GROUP(s) to SET_GROUP. -- @param Core.Set#SET_GROUP self -- @param #string AddGroupNames A single name or an array of GROUP names. -- @return self function SET_GROUP:AddGroupsByName( AddGroupNames ) - + local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames } - + for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do self:Add( AddGroupName, GROUP:FindByName( AddGroupName ) ) end - + return self end - + --- Remove GROUP(s) from SET_GROUP. -- @param Core.Set#SET_GROUP self -- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names. -- @return self function SET_GROUP:RemoveGroupsByName( RemoveGroupNames ) - + local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames } - + for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do self:Remove( RemoveGroupName ) end - + return self end - - - - + + + + --- Finds a Group based on the Group Name. -- @param #SET_GROUP self -- @param #string GroupName -- @return Wrapper.Group#GROUP The found Group. function SET_GROUP:FindGroup( GroupName ) - + local GroupFound = self.Set[GroupName] return GroupFound end - + --- Iterate the SET_GROUP while identifying the nearest object from a @{Core.Point#POINT_VEC2}. -- @param #SET_GROUP self -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest object in the set. -- @return Wrapper.Group#GROUP The closest group. function SET_GROUP:FindNearestGroupFromPointVec2( PointVec2 ) self:F2( PointVec2 ) - + local NearestGroup = nil --Wrapper.Group#GROUP local ClosestDistance = nil - + for ObjectID, ObjectData in pairs( self.Set ) do if NearestGroup == nil then - NearestGroup = ObjectData + NearestGroup = ObjectData ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) else local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) @@ -914,11 +914,11 @@ do -- SET_GROUP end end end - + return NearestGroup end - - + + --- Builds a set of groups of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_GROUP self @@ -936,8 +936,8 @@ do -- SET_GROUP end return self end - - + + --- Builds a set of groups out of categories. -- Possible current categories are plane, helicopter, ground, ship. -- @param #SET_GROUP self @@ -955,7 +955,7 @@ do -- SET_GROUP end return self end - + --- Builds a set of groups out of ground category. -- @param #SET_GROUP self -- @return #SET_GROUP self @@ -963,7 +963,7 @@ do -- SET_GROUP self:FilterCategories( "ground" ) return self end - + --- Builds a set of groups out of airplane category. -- @param #SET_GROUP self -- @return #SET_GROUP self @@ -971,7 +971,7 @@ do -- SET_GROUP self:FilterCategories( "plane" ) return self end - + --- Builds a set of groups out of helicopter category. -- @param #SET_GROUP self -- @return #SET_GROUP self @@ -979,7 +979,7 @@ do -- SET_GROUP self:FilterCategories( "helicopter" ) return self end - + --- Builds a set of groups out of ship category. -- @param #SET_GROUP self -- @return #SET_GROUP self @@ -987,7 +987,7 @@ do -- SET_GROUP self:FilterCategories( "ship" ) return self end - + --- Builds a set of groups out of structure category. -- @param #SET_GROUP self -- @return #SET_GROUP self @@ -995,9 +995,9 @@ do -- SET_GROUP self:FilterCategories( "structure" ) return self end - - - + + + --- Builds a set of groups of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_GROUP self @@ -1015,8 +1015,8 @@ do -- SET_GROUP end return self end - - + + --- Builds a set of groups of defined GROUP prefixes. -- All the groups starting with the given prefixes will be included within the set. -- @param #SET_GROUP self @@ -1034,7 +1034,7 @@ do -- SET_GROUP end return self end - + --- Builds a set of groups that are only active. -- Only the groups that are active will be included within the set. -- @param #SET_GROUP self @@ -1042,31 +1042,31 @@ do -- SET_GROUP -- Include inactive groups if you provide false. -- @return #SET_GROUP self -- @usage - -- + -- -- -- Include only active groups to the set. -- GroupSet = SET_GROUP:New():FilterActive():FilterStart() - -- + -- -- -- Include only active groups to the set of the blue coalition, and filter one time. -- GroupSet = SET_GROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() - -- + -- -- -- Include only active groups to the set of the blue coalition, and filter one time. -- -- Later, reset to include back inactive groups to the set. -- GroupSet = SET_GROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() -- ... logic ... -- GroupSet = SET_GROUP:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce() - -- + -- function SET_GROUP:FilterActive( Active ) Active = Active or not ( Active == false ) self.Filter.Active = Active return self end - - + + --- Starts the filtering. -- @param #SET_GROUP self -- @return #SET_GROUP self function SET_GROUP:FilterStart() - + if _DATABASE then self:_FilterStart() self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) @@ -1074,19 +1074,19 @@ do -- SET_GROUP self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash ) end - - - + + + return self end - + --- Handles the OnDead or OnCrash event for alive groups set. -- Note: The GROUP object in the SET_GROUP collection will only be removed if the last unit is destroyed of the GROUP. -- @param #SET_GROUP self -- @param Core.Event#EVENTDATA Event function SET_GROUP:_EventOnDeadOrCrash( Event ) self:F( { Event } ) - + if Event.IniDCSUnit then local ObjectName, Object = self:FindInDatabase( Event ) if ObjectName then @@ -1096,7 +1096,7 @@ do -- SET_GROUP end end end - + --- Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! -- @param #SET_GROUP self @@ -1105,17 +1105,17 @@ do -- SET_GROUP -- @return #table The GROUP function SET_GROUP:AddInDatabase( Event ) self:F3( { Event } ) - + if Event.IniObjectCategory == 1 then if not self.Database[Event.IniDCSGroupName] then self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) self:T3( self.Database[Event.IniDCSGroupName] ) end end - + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] end - + --- Handles the Database to check on any event that Object exists in the Database. -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! -- @param #SET_GROUP self @@ -1124,34 +1124,34 @@ do -- SET_GROUP -- @return #table The GROUP function SET_GROUP:FindInDatabase( Event ) self:F3( { Event } ) - + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] end - + --- Iterate the SET_GROUP and call an iterator function for each GROUP object, providing the GROUP and optional parameters. -- @param #SET_GROUP self -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. -- @return #SET_GROUP self function SET_GROUP:ForEachGroup( IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet() ) - + return self end - + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP object, providing the GROUP and optional parameters. -- @param #SET_GROUP self -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. -- @return #SET_GROUP self function SET_GROUP:ForEachGroupAlive( IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetAliveSet() ) - + return self end - + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -1159,7 +1159,7 @@ do -- SET_GROUP -- @return #SET_GROUP self function SET_GROUP:ForEachGroupCompletelyInZone( ZoneObject, IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet(), --- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Group#GROUP GroupObject @@ -1170,10 +1170,10 @@ do -- SET_GROUP return false end end, { ZoneObject } ) - + return self end - + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -1181,7 +1181,7 @@ do -- SET_GROUP -- @return #SET_GROUP self function SET_GROUP:ForEachGroupPartlyInZone( ZoneObject, IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet(), --- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Group#GROUP GroupObject @@ -1192,10 +1192,10 @@ do -- SET_GROUP return false end end, { ZoneObject } ) - + return self end - + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -1203,7 +1203,7 @@ do -- SET_GROUP -- @return #SET_GROUP self function SET_GROUP:ForEachGroupNotInZone( ZoneObject, IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet(), --- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Group#GROUP GroupObject @@ -1214,10 +1214,10 @@ do -- SET_GROUP return false end end, { ZoneObject } ) - + return self end - + --- Iterate the SET_GROUP and return true if all the @{Wrapper.Group#GROUP} are completely in the @{Core.Zone#ZONE} -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -1236,7 +1236,7 @@ do -- SET_GROUP self:F2(Zone) local Set = self:GetSet() for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if not GroupData:IsCompletelyInZone(Zone) then + if not GroupData:IsCompletelyInZone(Zone) then return false end end @@ -1250,7 +1250,7 @@ do -- SET_GROUP -- @return #SET_GROUP self function SET_GROUP:ForEachGroupAnyInZone( ZoneObject, IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet(), --- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Group#GROUP GroupObject @@ -1261,11 +1261,11 @@ do -- SET_GROUP return false end end, { ZoneObject } ) - + return self end - + --- Iterate the SET_GROUP and return true if at least one of the @{Wrapper.Group#GROUP} is completely inside the @{Core.Zone#ZONE} -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -1284,13 +1284,13 @@ do -- SET_GROUP self:F2(Zone) local Set = self:GetSet() for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsCompletelyInZone(Zone) then + if GroupData:IsCompletelyInZone(Zone) then return true end end return false end - + --- Iterate the SET_GROUP and return true if at least one @{#UNIT} of one @{GROUP} of the @{SET_GROUP} is in @{ZONE} -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -1309,13 +1309,13 @@ do -- SET_GROUP self:F2(Zone) local Set = self:GetSet() for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsPartlyInZone(Zone) or GroupData:IsCompletelyInZone(Zone) then + if GroupData:IsPartlyInZone(Zone) or GroupData:IsCompletelyInZone(Zone) then return true end end return false end - + --- Iterate the SET_GROUP and return true if at least one @{GROUP} of the @{SET_GROUP} is partly in @{ZONE}. -- Will return false if a @{GROUP} is fully in the @{ZONE} -- @param #SET_GROUP self @@ -1338,20 +1338,20 @@ do -- SET_GROUP for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP if GroupData:IsCompletelyInZone(Zone) then return false - elseif GroupData:IsPartlyInZone(Zone) then + elseif GroupData:IsPartlyInZone(Zone) then IsPartlyInZone = true -- at least one GROUP is partly in zone end end - + if IsPartlyInZone then return true else return false end end - + --- Iterate the SET_GROUP and return true if no @{GROUP} of the @{SET_GROUP} is in @{ZONE} - -- This could also be achieved with `not SET_GROUP:AnyPartlyInZone(Zone)`, but it's easier for the + -- This could also be achieved with `not SET_GROUP:AnyPartlyInZone(Zone)`, but it's easier for the -- mission designer to add a dedicated method -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -1376,7 +1376,7 @@ do -- SET_GROUP end return true end - + --- Iterate the SET_GROUP and count how many GROUPs are completely in the Zone -- That could easily be done with SET_GROUP:ForEachGroupCompletelyInZone(), but this function -- provides an easy to use shortcut... @@ -1394,13 +1394,13 @@ do -- SET_GROUP local Count = 0 local Set = self:GetSet() for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsCompletelyInZone(Zone) then + if GroupData:IsCompletelyInZone(Zone) then Count = Count + 1 end end return Count end - + --- Iterate the SET_GROUP and count how many UNITs are completely in the Zone -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -1420,16 +1420,16 @@ do -- SET_GROUP end return Count end - + ----- Iterate the SET_GROUP and call an interator function for each **alive** player, providing the Group of the player and optional parameters. ---- @param #SET_GROUP self ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. ---- @return #SET_GROUP self --function SET_GROUP:ForEachPlayer( IteratorFunction, ... ) -- self:F2( arg ) - -- + -- -- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) - -- + -- -- return self --end -- @@ -1440,13 +1440,13 @@ do -- SET_GROUP ---- @return #SET_GROUP self --function SET_GROUP:ForEachClient( IteratorFunction, ... ) -- self:F2( arg ) - -- + -- -- self:ForEach( IteratorFunction, arg, self.Clients ) -- -- return self --end - - + + --- -- @param #SET_GROUP self -- @param Wrapper.Group#GROUP MGroup The group that is checked for inclusion. @@ -1454,7 +1454,7 @@ do -- SET_GROUP function SET_GROUP:IsIncludeObject( MGroup ) self:F2( MGroup ) local MGroupInclude = true - + if self.Filter.Active ~= nil then local MGroupActive = false self:F( { Active = self.Filter.Active } ) @@ -1463,7 +1463,7 @@ do -- SET_GROUP end MGroupInclude = MGroupInclude and MGroupActive end - + if self.Filter.Coalitions then local MGroupCoalition = false for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do @@ -1474,7 +1474,7 @@ do -- SET_GROUP end MGroupInclude = MGroupInclude and MGroupCoalition end - + if self.Filter.Categories then local MGroupCategory = false for CategoryID, CategoryName in pairs( self.Filter.Categories ) do @@ -1485,7 +1485,7 @@ do -- SET_GROUP end MGroupInclude = MGroupInclude and MGroupCategory end - + if self.Filter.Countries then local MGroupCountry = false for CountryID, CountryName in pairs( self.Filter.Countries ) do @@ -1496,7 +1496,7 @@ do -- SET_GROUP end MGroupInclude = MGroupInclude and MGroupCountry end - + if self.Filter.GroupPrefixes then local MGroupPrefix = false for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do @@ -1507,12 +1507,12 @@ do -- SET_GROUP end MGroupInclude = MGroupInclude and MGroupPrefix end - + self:T2( MGroupInclude ) return MGroupInclude end - - + + --- Iterate the SET_GROUP and set for each unit the default cargo bay weight limit. -- Because within a group, the type of carriers can differ, each cargo bay weight limit is set on @{Wrapper.Unit} level. -- @param #SET_GROUP self @@ -1537,103 +1537,103 @@ do -- SET_UNIT --- @type SET_UNIT -- @extends Core.Set#SET_BASE - + --- Mission designers can use the SET_UNIT class to build sets of units belonging to certain: - -- + -- -- * Coalitions -- * Categories -- * Countries -- * Unit types -- * Starting with certain prefix strings. - -- + -- -- ## 1) SET_UNIT constructor -- -- Create a new SET_UNIT object with the @{#SET_UNIT.New} method: - -- + -- -- * @{#SET_UNIT.New}: Creates a new SET_UNIT object. - -- + -- -- ## 2) Add or Remove UNIT(s) from SET_UNIT -- - -- UNITs can be added and removed using the @{Core.Set#SET_UNIT.AddUnitsByName} and @{Core.Set#SET_UNIT.RemoveUnitsByName} respectively. + -- UNITs can be added and removed using the @{Core.Set#SET_UNIT.AddUnitsByName} and @{Core.Set#SET_UNIT.RemoveUnitsByName} respectively. -- These methods take a single UNIT name or an array of UNIT names to be added or removed from SET_UNIT. - -- + -- -- ## 3) SET_UNIT filter criteria - -- + -- -- You can set filter criteria to define the set of units within the SET_UNIT. -- Filter criteria are defined by: - -- + -- -- * @{#SET_UNIT.FilterCoalitions}: Builds the SET_UNIT with the units belonging to the coalition(s). -- * @{#SET_UNIT.FilterCategories}: Builds the SET_UNIT with the units belonging to the category(ies). -- * @{#SET_UNIT.FilterTypes}: Builds the SET_UNIT with the units belonging to the unit type(s). -- * @{#SET_UNIT.FilterCountries}: Builds the SET_UNIT with the units belonging to the country(ies). -- * @{#SET_UNIT.FilterPrefixes}: Builds the SET_UNIT with the units starting with the same prefix string(s). -- * @{#SET_UNIT.FilterActive}: Builds the SET_UNIT with the units that are only active. Units that are inactive (late activation) won't be included in the set! - -- + -- -- Once the filter criteria have been set for the SET_UNIT, you can start filtering using: - -- + -- -- * @{#SET_UNIT.FilterStart}: Starts the filtering of the units **dynamically**. -- * @{#SET_UNIT.FilterOnce}: Filters of the units **once**. - -- + -- -- Planned filter criteria within development are (so these are not yet available): - -- + -- -- * @{#SET_UNIT.FilterZones}: Builds the SET_UNIT with the units within a @{Core.Zone#ZONE}. - -- + -- -- ## 4) SET_UNIT iterators - -- + -- -- Once the filters have been defined and the SET_UNIT has been built, you can iterate the SET_UNIT with the available iterator methods. -- The iterator methods will walk the SET_UNIT set, and call for each element within the set a function that you provide. -- The following iterator methods are currently available within the SET_UNIT: - -- + -- -- * @{#SET_UNIT.ForEachUnit}: Calls a function for each alive unit it finds within the SET_UNIT. -- * @{#SET_UNIT.ForEachUnitInZone}: Iterate the SET_UNIT and call an iterator function for each **alive** UNIT object presence completely in a @{Zone}, providing the UNIT object and optional parameters to the called function. -- * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate the SET_UNIT and call an iterator function for each **alive** UNIT object presence not in a @{Zone}, providing the UNIT object and optional parameters to the called function. - -- + -- -- Planned iterators methods in development are (so these are not yet available): - -- + -- -- * @{#SET_UNIT.ForEachUnitInUnit}: Calls a function for each unit contained within the SET_UNIT. -- * @{#SET_UNIT.ForEachUnitCompletelyInZone}: Iterate and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. -- * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. - -- + -- -- ## 5) SET_UNIT atomic methods - -- + -- -- Various methods exist for a SET_UNIT to perform actions or calculations and retrieve results from the SET_UNIT: - -- + -- -- * @{#SET_UNIT.GetTypeNames}(): Retrieve the type names of the @{Wrapper.Unit}s in the SET, delimited by a comma. - -- + -- -- ## 6) SET_UNIT trigger events on the UNIT objects. - -- + -- -- The SET is derived from the FSM class, which provides extra capabilities to track the contents of the UNIT objects in the SET_UNIT. - -- + -- -- ### 6.1) When a UNIT object crashes or is dead, the SET_UNIT will trigger a **Dead** event. - -- - -- You can handle the event using the OnBefore and OnAfter event handlers. + -- + -- You can handle the event using the OnBefore and OnAfter event handlers. -- The event handlers need to have the paramters From, Event, To, GroupObject. -- The GroupObject is the UNIT object that is dead and within the SET_UNIT, and is passed as a parameter to the event handler. -- See the following example: - -- + -- -- -- Create the SetCarrier SET_UNIT collection. -- -- local SetHelicopter = SET_UNIT:New():FilterPrefixes( "Helicopter" ):FilterStart() - -- + -- -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier unit is destroyed, that all internal parameters are reset. -- -- function SetHelicopter:OnAfterDead( From, Event, To, UnitObject ) -- self:F( { UnitObject = UnitObject:GetName() } ) -- end - -- + -- -- While this is a good example, there is a catch. -- Imageine you want to execute the code above, the the self would need to be from the object declared outside (above) the OnAfterDead method. -- So, the self would need to contain another object. Fortunately, this can be done, but you must use then the **`.`** notation for the method. -- See the modified example: - -- + -- -- -- Now we have a constructor of the class AI_CARGO_DISPATCHER, that receives the SetHelicopter as a parameter. -- -- Within that constructor, we want to set an enclosed event handler OnAfterDead for SetHelicopter. -- -- But within the OnAfterDead method, we want to refer to the self variable of the AI_CARGO_DISPATCHER. - -- + -- -- function ACLASS:New( SetCarrier, SetCargo, SetDeployZones ) - -- + -- -- local self = BASE:Inherit( self, FSM:New() ) -- #AI_CARGO_DISPATCHER - -- + -- -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset. -- -- Note the "." notation, and the explicit declaration of SetHelicopter, which would be using the ":" notation the implicit self variable declaration. -- @@ -1641,7 +1641,7 @@ do -- SET_UNIT -- SetHelicopter:F( { UnitObject = UnitObject:GetName() } ) -- self.array[UnitObject] = nil -- So here I clear the array table entry of the self object ACLASS. -- end - -- + -- -- end -- === -- @field #SET_UNIT SET_UNIT @@ -1670,13 +1670,13 @@ do -- SET_UNIT }, }, } - - + + --- Get the first unit from the set. -- @function [parent=#SET_UNIT] GetFirst -- @param #SET_UNIT self -- @return Wrapper.Unit#UNIT The UNIT object. - + --- Creates a new SET_UNIT object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_UNIT self -- @return #SET_UNIT @@ -1684,75 +1684,75 @@ do -- SET_UNIT -- -- Define a new SET_UNIT Object. This DBObject will contain a reference to all alive Units. -- DBObject = SET_UNIT:New() function SET_UNIT:New() - + -- Inherits from BASE local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.UNITS ) ) -- #SET_UNIT - + self:FilterActive( false ) - + return self end - + --- Add UNIT(s) to SET_UNIT. -- @param #SET_UNIT self -- @param Wrapper.Unit#UNIT Unit A single UNIT. -- @return #SET_UNIT self function SET_UNIT:AddUnit( Unit ) self:F2( Unit:GetName() ) - + self:Add( Unit:GetName(), Unit ) - + -- Set the default cargo bay limit each time a new unit is added to the set. Unit:SetCargoBayWeightLimit() - + return self end - - + + --- Add UNIT(s) to SET_UNIT. -- @param #SET_UNIT self -- @param #string AddUnitNames A single name or an array of UNIT names. -- @return #SET_UNIT self function SET_UNIT:AddUnitsByName( AddUnitNames ) - + local AddUnitNamesArray = ( type( AddUnitNames ) == "table" ) and AddUnitNames or { AddUnitNames } - + self:T( AddUnitNamesArray ) for AddUnitID, AddUnitName in pairs( AddUnitNamesArray ) do self:Add( AddUnitName, UNIT:FindByName( AddUnitName ) ) end - + return self end - + --- Remove UNIT(s) from SET_UNIT. -- @param Core.Set#SET_UNIT self -- @param Wrapper.Unit#UNIT RemoveUnitNames A single name or an array of UNIT names. -- @return self function SET_UNIT:RemoveUnitsByName( RemoveUnitNames ) - + local RemoveUnitNamesArray = ( type( RemoveUnitNames ) == "table" ) and RemoveUnitNames or { RemoveUnitNames } - + for RemoveUnitID, RemoveUnitName in pairs( RemoveUnitNamesArray ) do self:Remove( RemoveUnitName ) end - + return self end - - + + --- Finds a Unit based on the Unit Name. -- @param #SET_UNIT self -- @param #string UnitName -- @return Wrapper.Unit#UNIT The found Unit. function SET_UNIT:FindUnit( UnitName ) - + local UnitFound = self.Set[UnitName] return UnitFound end - - - + + + --- Builds a set of units of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_UNIT self @@ -1769,8 +1769,8 @@ do -- SET_UNIT end return self end - - + + --- Builds a set of units out of categories. -- Possible current categories are plane, helicopter, ground, ship. -- @param #SET_UNIT self @@ -1788,8 +1788,8 @@ do -- SET_UNIT end return self end - - + + --- Builds a set of units of defined unit types. -- Possible current types are those types known within DCS world. -- @param #SET_UNIT self @@ -1807,8 +1807,8 @@ do -- SET_UNIT end return self end - - + + --- Builds a set of units of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_UNIT self @@ -1826,8 +1826,8 @@ do -- SET_UNIT end return self end - - + + --- Builds a set of units of defined unit prefixes. -- All the units starting with the given prefixes will be included within the set. -- @param #SET_UNIT self @@ -1845,7 +1845,7 @@ do -- SET_UNIT end return self end - + --- Builds a set of units that are only active. -- Only the units that are active will be included within the set. -- @param #SET_UNIT self @@ -1853,32 +1853,32 @@ do -- SET_UNIT -- Include inactive units if you provide false. -- @return #SET_UNIT self -- @usage - -- + -- -- -- Include only active units to the set. -- UnitSet = SET_UNIT:New():FilterActive():FilterStart() - -- + -- -- -- Include only active units to the set of the blue coalition, and filter one time. -- UnitSet = SET_UNIT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() - -- + -- -- -- Include only active units to the set of the blue coalition, and filter one time. -- -- Later, reset to include back inactive units to the set. -- UnitSet = SET_UNIT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() -- ... logic ... -- UnitSet = SET_UNIT:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce() - -- + -- function SET_UNIT:FilterActive( Active ) Active = Active or not ( Active == false ) self.Filter.Active = Active return self end - + --- Builds a set of units having a radar of give types. -- All the units having a radar of a given type will be included within the set. -- @param #SET_UNIT self -- @param #table RadarTypes The radar types. -- @return #SET_UNIT self function SET_UNIT:FilterHasRadar( RadarTypes ) - + self.Filter.RadarTypes = self.Filter.RadarTypes or {} if type( RadarTypes ) ~= "table" then RadarTypes = { RadarTypes } @@ -1888,23 +1888,23 @@ do -- SET_UNIT end return self end - + --- Builds a set of SEADable units. -- @param #SET_UNIT self -- @return #SET_UNIT self function SET_UNIT:FilterHasSEAD() - + self.Filter.SEAD = true return self end - - - + + + --- Starts the filtering. -- @param #SET_UNIT self -- @return #SET_UNIT self function SET_UNIT:FilterStart() - + if _DATABASE then self:_FilterStart() self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) @@ -1912,12 +1912,12 @@ do -- SET_UNIT self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash ) end - + return self end - - + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! -- @param #SET_UNIT self @@ -1926,17 +1926,17 @@ do -- SET_UNIT -- @return #table The UNIT function SET_UNIT:AddInDatabase( Event ) self:F3( { Event } ) - + if Event.IniObjectCategory == 1 then if not self.Database[Event.IniDCSUnitName] then self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) self:T3( self.Database[Event.IniDCSUnitName] ) end end - + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - + --- Handles the Database to check on any event that Object exists in the Database. -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! -- @param #SET_UNIT self @@ -1945,24 +1945,24 @@ do -- SET_UNIT -- @return #table The UNIT function SET_UNIT:FindInDatabase( Event ) self:F2( { Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event } ) - - + + return Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName] end - - + + do -- Is Zone methods - + --- Check if minimal one element of the SET_UNIT is in the Zone. -- @param #SET_UNIT self -- @param Core.Zone#ZONE ZoneTest The Zone to be tested for. -- @return #boolean function SET_UNIT:IsPartiallyInZone( ZoneTest ) - + local IsPartiallyInZone = false - + local function EvaluateZone( ZoneUnit ) - + local ZoneUnitName = ZoneUnit:GetName() self:F( { ZoneUnitName = ZoneUnitName } ) if self:FindUnit( ZoneUnitName ) then @@ -1970,103 +1970,103 @@ do -- SET_UNIT self:F( { Found = true } ) return false end - + return true end ZoneTest:SearchZone( EvaluateZone ) - + return IsPartiallyInZone end - - + + --- Check if no element of the SET_UNIT is in the Zone. -- @param #SET_UNIT self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @return #boolean function SET_UNIT:IsNotInZone( Zone ) - + local IsNotInZone = true - + local function EvaluateZone( ZoneUnit ) - + local ZoneUnitName = ZoneUnit:GetName() if self:FindUnit( ZoneUnitName ) then IsNotInZone = false return false end - + return true end - + Zone:SearchZone( EvaluateZone ) - + return IsNotInZone end - - + + --- Check if minimal one element of the SET_UNIT is in the Zone. -- @param #SET_UNIT self -- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. -- @return #SET_UNIT self function SET_UNIT:ForEachUnitInZone( IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet() ) - + return self end - - + + end - - + + --- Iterate the SET_UNIT and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. -- @param #SET_UNIT self -- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. -- @return #SET_UNIT self function SET_UNIT:ForEachUnit( IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet() ) - + return self end - + --- Iterate the SET_UNIT **sorted *per Threat Level** and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. - -- + -- -- @param #SET_UNIT self -- @param #number FromThreatLevel The TreatLevel to start the evaluation **From** (this must be a value between 0 and 10). -- @param #number ToThreatLevel The TreatLevel to stop the evaluation **To** (this must be a value between 0 and 10). -- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. -- @return #SET_UNIT self -- @usage - -- + -- -- UnitSet:ForEachUnitPerThreatLevel( 10, 0, -- -- @param Wrapper.Unit#UNIT UnitObject The UNIT object in the UnitSet, that will be passed to the local function for evaluation. -- function( UnitObject ) -- .. logic .. -- end -- ) - -- + -- function SET_UNIT:ForEachUnitPerThreatLevel( FromThreatLevel, ToThreatLevel, IteratorFunction, ... ) --R2.1 Threat Level implementation self:F2( arg ) - + local ThreatLevelSet = {} - + if self:Count() ~= 0 then for UnitName, UnitObject in pairs( self.Set ) do local Unit = UnitObject -- Wrapper.Unit#UNIT - + local ThreatLevel = Unit:GetThreatLevel() ThreatLevelSet[ThreatLevel] = ThreatLevelSet[ThreatLevel] or {} ThreatLevelSet[ThreatLevel].Set = ThreatLevelSet[ThreatLevel].Set or {} ThreatLevelSet[ThreatLevel].Set[UnitName] = UnitObject self:F( { ThreatLevel = ThreatLevel, ThreatLevelSet = ThreatLevelSet[ThreatLevel].Set } ) end - + local ThreatLevelIncrement = FromThreatLevel <= ToThreatLevel and 1 or -1 - + for ThreatLevel = FromThreatLevel, ToThreatLevel, ThreatLevelIncrement do self:F( { ThreatLevel = ThreatLevel } ) local ThreatLevelItem = ThreatLevelSet[ThreatLevel] @@ -2075,12 +2075,12 @@ do -- SET_UNIT end end end - + return self end - - - + + + --- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. -- @param #SET_UNIT self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -2088,7 +2088,7 @@ do -- SET_UNIT -- @return #SET_UNIT self function SET_UNIT:ForEachUnitCompletelyInZone( ZoneObject, IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet(), --- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Unit#UNIT UnitObject @@ -2099,10 +2099,10 @@ do -- SET_UNIT return false end end, { ZoneObject } ) - + return self end - + --- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. -- @param #SET_UNIT self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -2110,7 +2110,7 @@ do -- SET_UNIT -- @return #SET_UNIT self function SET_UNIT:ForEachUnitNotInZone( ZoneObject, IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet(), --- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Unit#UNIT UnitObject @@ -2121,24 +2121,24 @@ do -- SET_UNIT return false end end, { ZoneObject } ) - + return self end - + --- Returns map of unit types. -- @param #SET_UNIT self -- @return #map<#string,#number> A map of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. function SET_UNIT:GetUnitTypes() self:F2() - + local MT = {} -- Message Text local UnitTypes = {} - + for UnitID, UnitData in pairs( self:GetSet() ) do local TextUnit = UnitData -- Wrapper.Unit#UNIT if TextUnit:IsAlive() then local UnitType = TextUnit:GetTypeName() - + if not UnitTypes[UnitType] then UnitTypes[UnitType] = 1 else @@ -2146,60 +2146,60 @@ do -- SET_UNIT end end end - + for UnitTypeID, UnitType in pairs( UnitTypes ) do MT[#MT+1] = UnitType .. " of " .. UnitTypeID end - + return UnitTypes end - - + + --- Returns a comma separated string of the unit types with a count in the @{Set}. -- @param #SET_UNIT self -- @return #string The unit types string function SET_UNIT:GetUnitTypesText() self:F2() - + local MT = {} -- Message Text local UnitTypes = self:GetUnitTypes() - + for UnitTypeID, UnitType in pairs( UnitTypes ) do MT[#MT+1] = UnitType .. " of " .. UnitTypeID end - + return table.concat( MT, ", " ) end - + --- Returns map of unit threat levels. -- @param #SET_UNIT self -- @return #table. function SET_UNIT:GetUnitThreatLevels() self:F2() - + local UnitThreatLevels = {} - + for UnitID, UnitData in pairs( self:GetSet() ) do local ThreatUnit = UnitData -- Wrapper.Unit#UNIT if ThreatUnit:IsAlive() then local UnitThreatLevel, UnitThreatLevelText = ThreatUnit:GetThreatLevel() local ThreatUnitName = ThreatUnit:GetName() - + UnitThreatLevels[UnitThreatLevel] = UnitThreatLevels[UnitThreatLevel] or {} UnitThreatLevels[UnitThreatLevel].UnitThreatLevelText = UnitThreatLevelText UnitThreatLevels[UnitThreatLevel].Units = UnitThreatLevels[UnitThreatLevel].Units or {} UnitThreatLevels[UnitThreatLevel].Units[ThreatUnitName] = ThreatUnit end end - + return UnitThreatLevels end - + --- Calculate the maxium A2G threat level of the SET_UNIT. -- @param #SET_UNIT self -- @return #number The maximum threatlevel function SET_UNIT:CalculateThreatLevelA2G() - + local MaxThreatLevelA2G = 0 local MaxThreatText = "" for UnitName, UnitData in pairs( self:GetSet() ) do @@ -2210,19 +2210,19 @@ do -- SET_UNIT MaxThreatText = ThreatText end end - + self:F( { MaxThreatLevelA2G = MaxThreatLevelA2G, MaxThreatText = MaxThreatText } ) return MaxThreatLevelA2G, MaxThreatText - + end - + --- Get the center coordinate of the SET_UNIT. -- @param #SET_UNIT self -- @return Core.Point#COORDINATE The center coordinate of all the units in the set, including heading in degrees and speed in mps in case of moving units. function SET_UNIT:GetCoordinate() - + local Coordinate = self:GetFirst():GetCoordinate() - + local x1 = Coordinate.x local x2 = Coordinate.x local y1 = Coordinate.y @@ -2232,19 +2232,19 @@ do -- SET_UNIT local MaxVelocity = 0 local AvgHeading = nil local MovingCount = 0 - + for UnitName, UnitData in pairs( self:GetSet() ) do - + local Unit = UnitData -- Wrapper.Unit#UNIT local Coordinate = Unit:GetCoordinate() - + x1 = ( Coordinate.x < x1 ) and Coordinate.x or x1 x2 = ( Coordinate.x > x2 ) and Coordinate.x or x2 y1 = ( Coordinate.y < y1 ) and Coordinate.y or y1 y2 = ( Coordinate.y > y2 ) and Coordinate.y or y2 z1 = ( Coordinate.y < z1 ) and Coordinate.z or z1 z2 = ( Coordinate.y > z2 ) and Coordinate.z or z2 - + local Velocity = Coordinate:GetVelocity() if Velocity ~= 0 then MaxVelocity = ( MaxVelocity < Velocity ) and Velocity or MaxVelocity @@ -2253,58 +2253,58 @@ do -- SET_UNIT MovingCount = MovingCount + 1 end end - + AvgHeading = AvgHeading and ( AvgHeading / MovingCount ) - + Coordinate.x = ( x2 - x1 ) / 2 + x1 Coordinate.y = ( y2 - y1 ) / 2 + y1 Coordinate.z = ( z2 - z1 ) / 2 + z1 Coordinate:SetHeading( AvgHeading ) Coordinate:SetVelocity( MaxVelocity ) - + self:F( { Coordinate = Coordinate } ) return Coordinate - + end - + --- Get the maximum velocity of the SET_UNIT. -- @param #SET_UNIT self -- @return #number The speed in mps in case of moving units. function SET_UNIT:GetVelocity() - + local Coordinate = self:GetFirst():GetCoordinate() - + local MaxVelocity = 0 - + for UnitName, UnitData in pairs( self:GetSet() ) do - + local Unit = UnitData -- Wrapper.Unit#UNIT local Coordinate = Unit:GetCoordinate() - + local Velocity = Coordinate:GetVelocity() if Velocity ~= 0 then MaxVelocity = ( MaxVelocity < Velocity ) and Velocity or MaxVelocity end end - + self:F( { MaxVelocity = MaxVelocity } ) return MaxVelocity - + end - + --- Get the average heading of the SET_UNIT. -- @param #SET_UNIT self -- @return #number Heading Heading in degrees and speed in mps in case of moving units. function SET_UNIT:GetHeading() - + local HeadingSet = nil local MovingCount = 0 - + for UnitName, UnitData in pairs( self:GetSet() ) do - + local Unit = UnitData -- Wrapper.Unit#UNIT local Coordinate = Unit:GetCoordinate() - + local Velocity = Coordinate:GetVelocity() if Velocity ~= 0 then local Heading = Coordinate:GetHeading() @@ -2317,23 +2317,23 @@ do -- SET_UNIT HeadingSet = nil break end - end + end end end - + return HeadingSet - + end - - - + + + --- Returns if the @{Set} has targets having a radar (of a given type). -- @param #SET_UNIT self -- @param DCS#Unit.RadarType RadarType -- @return #number The amount of radars in the Set with the given type function SET_UNIT:HasRadar( RadarType ) self:F2( RadarType ) - + local RadarCount = 0 for UnitID, UnitData in pairs( self:GetSet()) do local UnitSensorTest = UnitData -- Wrapper.Unit#UNIT @@ -2348,40 +2348,40 @@ do -- SET_UNIT RadarCount = RadarCount + 1 end end - + return RadarCount end - + --- Returns if the @{Set} has targets that can be SEADed. -- @param #SET_UNIT self -- @return #number The amount of SEADable units in the Set function SET_UNIT:HasSEAD() self:F2() - + local SEADCount = 0 for UnitID, UnitData in pairs( self:GetSet()) do local UnitSEAD = UnitData -- Wrapper.Unit#UNIT if UnitSEAD:IsAlive() then local UnitSEADAttributes = UnitSEAD:GetDesc().attributes - + local HasSEAD = UnitSEAD:HasSEAD() - + self:T3(HasSEAD) if HasSEAD then SEADCount = SEADCount + 1 end end end - + return SEADCount end - + --- Returns if the @{Set} has ground targets. -- @param #SET_UNIT self -- @return #number The amount of ground targets in the Set. function SET_UNIT:HasGroundUnits() self:F2() - + local GroundUnitCount = 0 for UnitID, UnitData in pairs( self:GetSet()) do local UnitTest = UnitData -- Wrapper.Unit#UNIT @@ -2389,16 +2389,16 @@ do -- SET_UNIT GroundUnitCount = GroundUnitCount + 1 end end - + return GroundUnitCount end - + --- Returns if the @{Set} has friendly ground units. -- @param #SET_UNIT self -- @return #number The amount of ground targets in the Set. function SET_UNIT:HasFriendlyUnits( FriendlyCoalition ) self:F2() - + local FriendlyUnitCount = 0 for UnitID, UnitData in pairs( self:GetSet()) do local UnitTest = UnitData -- Wrapper.Unit#UNIT @@ -2406,21 +2406,21 @@ do -- SET_UNIT FriendlyUnitCount = FriendlyUnitCount + 1 end end - + return FriendlyUnitCount end - - - + + + ----- Iterate the SET_UNIT and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. ---- @param #SET_UNIT self ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter. ---- @return #SET_UNIT self --function SET_UNIT:ForEachPlayer( IteratorFunction, ... ) -- self:F2( arg ) - -- + -- -- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) - -- + -- -- return self --end -- @@ -2431,13 +2431,13 @@ do -- SET_UNIT ---- @return #SET_UNIT self --function SET_UNIT:ForEachClient( IteratorFunction, ... ) -- self:F2( arg ) - -- + -- -- self:ForEach( IteratorFunction, arg, self.Clients ) -- -- return self --end - - + + --- -- @param #SET_UNIT self -- @param Wrapper.Unit#UNIT MUnit @@ -2448,9 +2448,9 @@ do -- SET_UNIT local MUnitInclude = false if MUnit:IsAlive() ~= nil then - + MUnitInclude = true - + if self.Filter.Active ~= nil then local MUnitActive = false if self.Filter.Active == false or ( self.Filter.Active == true and MUnit:IsActive() == true ) then @@ -2458,7 +2458,7 @@ do -- SET_UNIT end MUnitInclude = MUnitInclude and MUnitActive end - + if self.Filter.Coalitions then local MUnitCoalition = false for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do @@ -2469,7 +2469,7 @@ do -- SET_UNIT end MUnitInclude = MUnitInclude and MUnitCoalition end - + if self.Filter.Categories then local MUnitCategory = false for CategoryID, CategoryName in pairs( self.Filter.Categories ) do @@ -2480,7 +2480,7 @@ do -- SET_UNIT end MUnitInclude = MUnitInclude and MUnitCategory end - + if self.Filter.Types then local MUnitType = false for TypeID, TypeName in pairs( self.Filter.Types ) do @@ -2491,7 +2491,7 @@ do -- SET_UNIT end MUnitInclude = MUnitInclude and MUnitType end - + if self.Filter.Countries then local MUnitCountry = false for CountryID, CountryName in pairs( self.Filter.Countries ) do @@ -2502,7 +2502,7 @@ do -- SET_UNIT end MUnitInclude = MUnitInclude and MUnitCountry end - + if self.Filter.UnitPrefixes then local MUnitPrefix = false for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do @@ -2513,7 +2513,7 @@ do -- SET_UNIT end MUnitInclude = MUnitInclude and MUnitPrefix end - + if self.Filter.RadarTypes then local MUnitRadar = false for RadarTypeID, RadarType in pairs( self.Filter.RadarTypes ) do @@ -2527,7 +2527,7 @@ do -- SET_UNIT end MUnitInclude = MUnitInclude and MUnitRadar end - + if self.Filter.SEAD then local MUnitSEAD = false if MUnit:HasSEAD() == true then @@ -2537,33 +2537,33 @@ do -- SET_UNIT MUnitInclude = MUnitInclude and MUnitSEAD end end - + self:T2( MUnitInclude ) return MUnitInclude end - - + + --- Retrieve the type names of the @{Wrapper.Unit}s in the SET, delimited by an optional delimiter. -- @param #SET_UNIT self -- @param #string Delimiter (optional) The delimiter, which is default a comma. -- @return #string The types of the @{Wrapper.Unit}s delimited. function SET_UNIT:GetTypeNames( Delimiter ) - + Delimiter = Delimiter or ", " local TypeReport = REPORT:New() local Types = {} - + for UnitName, UnitData in pairs( self:GetSet() ) do - + local Unit = UnitData -- Wrapper.Unit#UNIT local UnitTypeName = Unit:GetTypeName() - + if not Types[UnitTypeName] then Types[UnitTypeName] = UnitTypeName TypeReport:Add( UnitTypeName ) end end - + return TypeReport:Text( Delimiter ) end @@ -2582,7 +2582,7 @@ do -- SET_UNIT end - + end @@ -2590,67 +2590,67 @@ do -- SET_STATIC --- @type SET_STATIC -- @extends Core.Set#SET_BASE - + --- Mission designers can use the SET_STATIC class to build sets of Statics belonging to certain: - -- + -- -- * Coalitions -- * Categories -- * Countries -- * Static types -- * Starting with certain prefix strings. - -- + -- -- ## SET_STATIC constructor -- -- Create a new SET_STATIC object with the @{#SET_STATIC.New} method: - -- + -- -- * @{#SET_STATIC.New}: Creates a new SET_STATIC object. - -- + -- -- ## Add or Remove STATIC(s) from SET_STATIC -- - -- STATICs can be added and removed using the @{Core.Set#SET_STATIC.AddStaticsByName} and @{Core.Set#SET_STATIC.RemoveStaticsByName} respectively. + -- STATICs can be added and removed using the @{Core.Set#SET_STATIC.AddStaticsByName} and @{Core.Set#SET_STATIC.RemoveStaticsByName} respectively. -- These methods take a single STATIC name or an array of STATIC names to be added or removed from SET_STATIC. - -- + -- -- ## SET_STATIC filter criteria - -- + -- -- You can set filter criteria to define the set of units within the SET_STATIC. -- Filter criteria are defined by: - -- + -- -- * @{#SET_STATIC.FilterCoalitions}: Builds the SET_STATIC with the units belonging to the coalition(s). -- * @{#SET_STATIC.FilterCategories}: Builds the SET_STATIC with the units belonging to the category(ies). -- * @{#SET_STATIC.FilterTypes}: Builds the SET_STATIC with the units belonging to the unit type(s). -- * @{#SET_STATIC.FilterCountries}: Builds the SET_STATIC with the units belonging to the country(ies). -- * @{#SET_STATIC.FilterPrefixes}: Builds the SET_STATIC with the units starting with the same prefix string(s). - -- + -- -- Once the filter criteria have been set for the SET_STATIC, you can start filtering using: - -- + -- -- * @{#SET_STATIC.FilterStart}: Starts the filtering of the units within the SET_STATIC. - -- + -- -- Planned filter criteria within development are (so these are not yet available): - -- + -- -- * @{#SET_STATIC.FilterZones}: Builds the SET_STATIC with the units within a @{Core.Zone#ZONE}. - -- + -- -- ## SET_STATIC iterators - -- + -- -- Once the filters have been defined and the SET_STATIC has been built, you can iterate the SET_STATIC with the available iterator methods. -- The iterator methods will walk the SET_STATIC set, and call for each element within the set a function that you provide. -- The following iterator methods are currently available within the SET_STATIC: - -- + -- -- * @{#SET_STATIC.ForEachStatic}: Calls a function for each alive unit it finds within the SET_STATIC. -- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. -- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. - -- + -- -- Planned iterators methods in development are (so these are not yet available): - -- + -- -- * @{#SET_STATIC.ForEachStaticInZone}: Calls a function for each unit contained within the SET_STATIC. -- * @{#SET_STATIC.ForEachStaticCompletelyInZone}: Iterate and call an iterator function for each **alive** STATIC presence completely in a @{Zone}, providing the STATIC and optional parameters to the called function. -- * @{#SET_STATIC.ForEachStaticNotInZone}: Iterate and call an iterator function for each **alive** STATIC presence not in a @{Zone}, providing the STATIC and optional parameters to the called function. - -- + -- -- ## SET_STATIC atomic methods - -- + -- -- Various methods exist for a SET_STATIC to perform actions or calculations and retrieve results from the SET_STATIC: - -- + -- -- * @{#SET_STATIC.GetTypeNames}(): Retrieve the type names of the @{Static}s in the SET, delimited by a comma. - -- + -- -- === -- @field #SET_STATIC SET_STATIC SET_STATIC = { @@ -2678,13 +2678,13 @@ do -- SET_STATIC }, }, } - - + + --- Get the first unit from the set. -- @function [parent=#SET_STATIC] GetFirst -- @param #SET_STATIC self -- @return Wrapper.Static#STATIC The STATIC object. - + --- Creates a new SET_STATIC object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_STATIC self -- @return #SET_STATIC @@ -2692,70 +2692,70 @@ do -- SET_STATIC -- -- Define a new SET_STATIC Object. This DBObject will contain a reference to all alive Statics. -- DBObject = SET_STATIC:New() function SET_STATIC:New() - + -- Inherits from BASE local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.STATICS ) ) -- Core.Set#SET_STATIC - + return self end - + --- Add STATIC(s) to SET_STATIC. -- @param #SET_STATIC self -- @param #string AddStatic A single STATIC. -- @return #SET_STATIC self function SET_STATIC:AddStatic( AddStatic ) self:F2( AddStatic:GetName() ) - + self:Add( AddStatic:GetName(), AddStatic ) - + return self end - - + + --- Add STATIC(s) to SET_STATIC. -- @param #SET_STATIC self -- @param #string AddStaticNames A single name or an array of STATIC names. -- @return #SET_STATIC self function SET_STATIC:AddStaticsByName( AddStaticNames ) - + local AddStaticNamesArray = ( type( AddStaticNames ) == "table" ) and AddStaticNames or { AddStaticNames } - + self:T( AddStaticNamesArray ) for AddStaticID, AddStaticName in pairs( AddStaticNamesArray ) do self:Add( AddStaticName, STATIC:FindByName( AddStaticName ) ) end - + return self end - + --- Remove STATIC(s) from SET_STATIC. -- @param Core.Set#SET_STATIC self -- @param Wrapper.Static#STATIC RemoveStaticNames A single name or an array of STATIC names. -- @return self function SET_STATIC:RemoveStaticsByName( RemoveStaticNames ) - + local RemoveStaticNamesArray = ( type( RemoveStaticNames ) == "table" ) and RemoveStaticNames or { RemoveStaticNames } - + for RemoveStaticID, RemoveStaticName in pairs( RemoveStaticNamesArray ) do self:Remove( RemoveStaticName ) end - + return self end - - + + --- Finds a Static based on the Static Name. -- @param #SET_STATIC self -- @param #string StaticName -- @return Wrapper.Static#STATIC The found Static. function SET_STATIC:FindStatic( StaticName ) - + local StaticFound = self.Set[StaticName] return StaticFound end - - - + + + --- Builds a set of units of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_STATIC self @@ -2773,8 +2773,8 @@ do -- SET_STATIC end return self end - - + + --- Builds a set of units out of categories. -- Possible current categories are plane, helicopter, ground, ship. -- @param #SET_STATIC self @@ -2792,8 +2792,8 @@ do -- SET_STATIC end return self end - - + + --- Builds a set of units of defined unit types. -- Possible current types are those types known within DCS world. -- @param #SET_STATIC self @@ -2811,8 +2811,8 @@ do -- SET_STATIC end return self end - - + + --- Builds a set of units of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_STATIC self @@ -2830,8 +2830,8 @@ do -- SET_STATIC end return self end - - + + --- Builds a set of units of defined unit prefixes. -- All the units starting with the given prefixes will be included within the set. -- @param #SET_STATIC self @@ -2849,23 +2849,23 @@ do -- SET_STATIC end return self end - - + + --- Starts the filtering. -- @param #SET_STATIC self -- @return #SET_STATIC self function SET_STATIC:FilterStart() - + if _DATABASE then self:_FilterStart() self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) end - + return self end - + --- Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! -- @param #SET_STATIC self @@ -2874,17 +2874,17 @@ do -- SET_STATIC -- @return #table The STATIC function SET_STATIC:AddInDatabase( Event ) self:F3( { Event } ) - + if Event.IniObjectCategory == Object.Category.STATIC then if not self.Database[Event.IniDCSStaticName] then self.Database[Event.IniDCSStaticName] = STATIC:Register( Event.IniDCSStaticName ) self:T3( self.Database[Event.IniDCSStaticName] ) end end - + return Event.IniDCSStaticName, self.Database[Event.IniDCSStaticName] end - + --- Handles the Database to check on any event that Object exists in the Database. -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! -- @param #SET_STATIC self @@ -2893,91 +2893,91 @@ do -- SET_STATIC -- @return #table The STATIC function SET_STATIC:FindInDatabase( Event ) self:F2( { Event.IniDCSStaticName, self.Set[Event.IniDCSStaticName], Event } ) - - + + return Event.IniDCSStaticName, self.Set[Event.IniDCSStaticName] end - - + + do -- Is Zone methods - + --- Check if minimal one element of the SET_STATIC is in the Zone. -- @param #SET_STATIC self -- @param Core.Zone#ZONE Zone The Zone to be tested for. -- @return #boolean function SET_STATIC:IsPatriallyInZone( Zone ) - + local IsPartiallyInZone = false - + local function EvaluateZone( ZoneStatic ) - + local ZoneStaticName = ZoneStatic:GetName() if self:FindStatic( ZoneStaticName ) then IsPartiallyInZone = true return false end - + return true end - + return IsPartiallyInZone end - - + + --- Check if no element of the SET_STATIC is in the Zone. -- @param #SET_STATIC self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @return #boolean function SET_STATIC:IsNotInZone( Zone ) - + local IsNotInZone = true - + local function EvaluateZone( ZoneStatic ) - + local ZoneStaticName = ZoneStatic:GetName() if self:FindStatic( ZoneStaticName ) then IsNotInZone = false return false end - + return true end - + Zone:Search( EvaluateZone ) - + return IsNotInZone end - - + + --- Check if minimal one element of the SET_STATIC is in the Zone. -- @param #SET_STATIC self -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. -- @return #SET_STATIC self function SET_STATIC:ForEachStaticInZone( IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet() ) - + return self end - - + + end - - + + --- Iterate the SET_STATIC and call an interator function for each **alive** STATIC, providing the STATIC and optional parameters. -- @param #SET_STATIC self -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. -- @return #SET_STATIC self function SET_STATIC:ForEachStatic( IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet() ) - + return self end - - + + --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence completely in a @{Zone}, providing the STATIC and optional parameters to the called function. -- @param #SET_STATIC self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -2985,7 +2985,7 @@ do -- SET_STATIC -- @return #SET_STATIC self function SET_STATIC:ForEachStaticCompletelyInZone( ZoneObject, IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet(), --- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Static#STATIC StaticObject @@ -2996,10 +2996,10 @@ do -- SET_STATIC return false end end, { ZoneObject } ) - + return self end - + --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence not in a @{Zone}, providing the STATIC and optional parameters to the called function. -- @param #SET_STATIC self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -3007,7 +3007,7 @@ do -- SET_STATIC -- @return #SET_STATIC self function SET_STATIC:ForEachStaticNotInZone( ZoneObject, IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet(), --- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Static#STATIC StaticObject @@ -3018,24 +3018,24 @@ do -- SET_STATIC return false end end, { ZoneObject } ) - + return self end - + --- Returns map of unit types. -- @param #SET_STATIC self -- @return #map<#string,#number> A map of the unit types found. The key is the StaticTypeName and the value is the amount of unit types found. function SET_STATIC:GetStaticTypes() self:F2() - + local MT = {} -- Message Text local StaticTypes = {} - + for StaticID, StaticData in pairs( self:GetSet() ) do local TextStatic = StaticData -- Wrapper.Static#STATIC if TextStatic:IsAlive() then local StaticType = TextStatic:GetTypeName() - + if not StaticTypes[StaticType] then StaticTypes[StaticType] = 1 else @@ -3043,38 +3043,38 @@ do -- SET_STATIC end end end - + for StaticTypeID, StaticType in pairs( StaticTypes ) do MT[#MT+1] = StaticType .. " of " .. StaticTypeID end - + return StaticTypes end - - + + --- Returns a comma separated string of the unit types with a count in the @{Set}. -- @param #SET_STATIC self -- @return #string The unit types string function SET_STATIC:GetStaticTypesText() self:F2() - + local MT = {} -- Message Text local StaticTypes = self:GetStaticTypes() - + for StaticTypeID, StaticType in pairs( StaticTypes ) do MT[#MT+1] = StaticType .. " of " .. StaticTypeID end - + return table.concat( MT, ", " ) end - + --- Get the center coordinate of the SET_STATIC. -- @param #SET_STATIC self -- @return Core.Point#COORDINATE The center coordinate of all the units in the set, including heading in degrees and speed in mps in case of moving units. function SET_STATIC:GetCoordinate() - + local Coordinate = self:GetFirst():GetCoordinate() - + local x1 = Coordinate.x local x2 = Coordinate.x local y1 = Coordinate.y @@ -3084,19 +3084,19 @@ do -- SET_STATIC local MaxVelocity = 0 local AvgHeading = nil local MovingCount = 0 - + for StaticName, StaticData in pairs( self:GetSet() ) do - + local Static = StaticData -- Wrapper.Static#STATIC local Coordinate = Static:GetCoordinate() - + x1 = ( Coordinate.x < x1 ) and Coordinate.x or x1 x2 = ( Coordinate.x > x2 ) and Coordinate.x or x2 y1 = ( Coordinate.y < y1 ) and Coordinate.y or y1 y2 = ( Coordinate.y > y2 ) and Coordinate.y or y2 z1 = ( Coordinate.y < z1 ) and Coordinate.z or z1 z2 = ( Coordinate.y > z2 ) and Coordinate.z or z2 - + local Velocity = Coordinate:GetVelocity() if Velocity ~= 0 then MaxVelocity = ( MaxVelocity < Velocity ) and Velocity or MaxVelocity @@ -3105,42 +3105,42 @@ do -- SET_STATIC MovingCount = MovingCount + 1 end end - + AvgHeading = AvgHeading and ( AvgHeading / MovingCount ) - + Coordinate.x = ( x2 - x1 ) / 2 + x1 Coordinate.y = ( y2 - y1 ) / 2 + y1 Coordinate.z = ( z2 - z1 ) / 2 + z1 Coordinate:SetHeading( AvgHeading ) Coordinate:SetVelocity( MaxVelocity ) - + self:F( { Coordinate = Coordinate } ) return Coordinate - + end - + --- Get the maximum velocity of the SET_STATIC. -- @param #SET_STATIC self -- @return #number The speed in mps in case of moving units. function SET_STATIC:GetVelocity() - + return 0 - + end - + --- Get the average heading of the SET_STATIC. -- @param #SET_STATIC self -- @return #number Heading Heading in degrees and speed in mps in case of moving units. function SET_STATIC:GetHeading() - + local HeadingSet = nil local MovingCount = 0 - + for StaticName, StaticData in pairs( self:GetSet() ) do - + local Static = StaticData -- Wrapper.Static#STATIC local Coordinate = Static:GetCoordinate() - + local Velocity = Coordinate:GetVelocity() if Velocity ~= 0 then local Heading = Coordinate:GetHeading() @@ -3153,19 +3153,19 @@ do -- SET_STATIC HeadingSet = nil break end - end + end end end - + return HeadingSet - + end - + --- Calculate the maxium A2G threat level of the SET_STATIC. -- @param #SET_STATIC self -- @return #number The maximum threatlevel function SET_STATIC:CalculateThreatLevelA2G() - + local MaxThreatLevelA2G = 0 local MaxThreatText = "" for StaticName, StaticData in pairs( self:GetSet() ) do @@ -3176,12 +3176,12 @@ do -- SET_STATIC MaxThreatText = ThreatText end end - + self:F( { MaxThreatLevelA2G = MaxThreatLevelA2G, MaxThreatText = MaxThreatText } ) return MaxThreatLevelA2G, MaxThreatText - + end - + --- -- @param #SET_STATIC self -- @param Wrapper.Static#STATIC MStatic @@ -3189,7 +3189,7 @@ do -- SET_STATIC function SET_STATIC:IsIncludeObject( MStatic ) self:F2( MStatic ) local MStaticInclude = true - + if self.Filter.Coalitions then local MStaticCoalition = false for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do @@ -3200,7 +3200,7 @@ do -- SET_STATIC end MStaticInclude = MStaticInclude and MStaticCoalition end - + if self.Filter.Categories then local MStaticCategory = false for CategoryID, CategoryName in pairs( self.Filter.Categories ) do @@ -3211,7 +3211,7 @@ do -- SET_STATIC end MStaticInclude = MStaticInclude and MStaticCategory end - + if self.Filter.Types then local MStaticType = false for TypeID, TypeName in pairs( self.Filter.Types ) do @@ -3222,7 +3222,7 @@ do -- SET_STATIC end MStaticInclude = MStaticInclude and MStaticType end - + if self.Filter.Countries then local MStaticCountry = false for CountryID, CountryName in pairs( self.Filter.Countries ) do @@ -3233,7 +3233,7 @@ do -- SET_STATIC end MStaticInclude = MStaticInclude and MStaticCountry end - + if self.Filter.StaticPrefixes then local MStaticPrefix = false for StaticPrefixId, StaticPrefix in pairs( self.Filter.StaticPrefixes ) do @@ -3244,36 +3244,36 @@ do -- SET_STATIC end MStaticInclude = MStaticInclude and MStaticPrefix end - + self:T2( MStaticInclude ) return MStaticInclude end - - + + --- Retrieve the type names of the @{Static}s in the SET, delimited by an optional delimiter. -- @param #SET_STATIC self -- @param #string Delimiter (optional) The delimiter, which is default a comma. -- @return #string The types of the @{Static}s delimited. function SET_STATIC:GetTypeNames( Delimiter ) - + Delimiter = Delimiter or ", " local TypeReport = REPORT:New() local Types = {} - + for StaticName, StaticData in pairs( self:GetSet() ) do - + local Static = StaticData -- Wrapper.Static#STATIC local StaticTypeName = Static:GetTypeName() - + if not Types[StaticTypeName] then Types[StaticTypeName] = StaticTypeName TypeReport:Add( StaticTypeName ) end end - + return TypeReport:Text( Delimiter ) end - + end @@ -3282,59 +3282,59 @@ do -- SET_CLIENT --- @type SET_CLIENT -- @extends Core.Set#SET_BASE - - - + + + --- Mission designers can use the @{Core.Set#SET_CLIENT} class to build sets of units belonging to certain: - -- + -- -- * Coalitions -- * Categories -- * Countries -- * Client types -- * Starting with certain prefix strings. - -- + -- -- ## 1) SET_CLIENT constructor - -- + -- -- Create a new SET_CLIENT object with the @{#SET_CLIENT.New} method: - -- + -- -- * @{#SET_CLIENT.New}: Creates a new SET_CLIENT object. - -- - -- ## 2) Add or Remove CLIENT(s) from SET_CLIENT - -- - -- CLIENTs can be added and removed using the @{Core.Set#SET_CLIENT.AddClientsByName} and @{Core.Set#SET_CLIENT.RemoveClientsByName} respectively. + -- + -- ## 2) Add or Remove CLIENT(s) from SET_CLIENT + -- + -- CLIENTs can be added and removed using the @{Core.Set#SET_CLIENT.AddClientsByName} and @{Core.Set#SET_CLIENT.RemoveClientsByName} respectively. -- These methods take a single CLIENT name or an array of CLIENT names to be added or removed from SET_CLIENT. - -- + -- -- ## 3) SET_CLIENT filter criteria - -- + -- -- You can set filter criteria to define the set of clients within the SET_CLIENT. -- Filter criteria are defined by: - -- + -- -- * @{#SET_CLIENT.FilterCoalitions}: Builds the SET_CLIENT with the clients belonging to the coalition(s). -- * @{#SET_CLIENT.FilterCategories}: Builds the SET_CLIENT with the clients belonging to the category(ies). -- * @{#SET_CLIENT.FilterTypes}: Builds the SET_CLIENT with the clients belonging to the client type(s). -- * @{#SET_CLIENT.FilterCountries}: Builds the SET_CLIENT with the clients belonging to the country(ies). -- * @{#SET_CLIENT.FilterPrefixes}: Builds the SET_CLIENT with the clients starting with the same prefix string(s). -- * @{#SET_CLIENT.FilterActive}: Builds the SET_CLIENT with the units that are only active. Units that are inactive (late activation) won't be included in the set! - -- + -- -- Once the filter criteria have been set for the SET_CLIENT, you can start filtering using: - -- + -- -- * @{#SET_CLIENT.FilterStart}: Starts the filtering of the clients **dynamically**. -- * @{#SET_CLIENT.FilterOnce}: Filters the clients **once**. - -- + -- -- Planned filter criteria within development are (so these are not yet available): - -- + -- -- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Core.Zone#ZONE}. - -- + -- -- ## 4) SET_CLIENT iterators - -- + -- -- Once the filters have been defined and the SET_CLIENT has been built, you can iterate the SET_CLIENT with the available iterator methods. -- The iterator methods will walk the SET_CLIENT set, and call for each element within the set a function that you provide. -- The following iterator methods are currently available within the SET_CLIENT: - -- + -- -- * @{#SET_CLIENT.ForEachClient}: Calls a function for each alive client it finds within the SET_CLIENT. - -- + -- -- === - -- @field #SET_CLIENT SET_CLIENT + -- @field #SET_CLIENT SET_CLIENT SET_CLIENT = { ClassName = "SET_CLIENT", Clients = {}, @@ -3360,8 +3360,8 @@ do -- SET_CLIENT }, }, } - - + + --- Creates a new SET_CLIENT object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_CLIENT self -- @return #SET_CLIENT @@ -3371,55 +3371,55 @@ do -- SET_CLIENT function SET_CLIENT:New() -- Inherits from BASE local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CLIENTS ) ) -- #SET_CLIENT - + self:FilterActive( false ) - + return self end - + --- Add CLIENT(s) to SET_CLIENT. -- @param Core.Set#SET_CLIENT self -- @param #string AddClientNames A single name or an array of CLIENT names. -- @return self function SET_CLIENT:AddClientsByName( AddClientNames ) - + local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } - + for AddClientID, AddClientName in pairs( AddClientNamesArray ) do self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) end - + return self end - + --- Remove CLIENT(s) from SET_CLIENT. -- @param Core.Set#SET_CLIENT self -- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. -- @return self function SET_CLIENT:RemoveClientsByName( RemoveClientNames ) - + local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } - + for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do self:Remove( RemoveClientName.ClientName ) end - + return self end - - + + --- Finds a Client based on the Client Name. -- @param #SET_CLIENT self -- @param #string ClientName -- @return Wrapper.Client#CLIENT The found Client. function SET_CLIENT:FindClient( ClientName ) - + local ClientFound = self.Set[ClientName] return ClientFound end - - - + + + --- Builds a set of clients of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_CLIENT self @@ -3437,8 +3437,8 @@ do -- SET_CLIENT end return self end - - + + --- Builds a set of clients out of categories. -- Possible current categories are plane, helicopter, ground, ship. -- @param #SET_CLIENT self @@ -3456,8 +3456,8 @@ do -- SET_CLIENT end return self end - - + + --- Builds a set of clients of defined client types. -- Possible current types are those types known within DCS world. -- @param #SET_CLIENT self @@ -3475,8 +3475,8 @@ do -- SET_CLIENT end return self end - - + + --- Builds a set of clients of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_CLIENT self @@ -3494,8 +3494,8 @@ do -- SET_CLIENT end return self end - - + + --- Builds a set of clients of defined client prefixes. -- All the clients starting with the given prefixes will be included within the set. -- @param #SET_CLIENT self @@ -3513,7 +3513,7 @@ do -- SET_CLIENT end return self end - + --- Builds a set of clients that are only active. -- Only the clients that are active will be included within the set. -- @param #SET_CLIENT self @@ -3521,42 +3521,42 @@ do -- SET_CLIENT -- Include inactive clients if you provide false. -- @return #SET_CLIENT self -- @usage - -- + -- -- -- Include only active clients to the set. -- ClientSet = SET_CLIENT:New():FilterActive():FilterStart() - -- + -- -- -- Include only active clients to the set of the blue coalition, and filter one time. -- ClientSet = SET_CLIENT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() - -- + -- -- -- Include only active clients to the set of the blue coalition, and filter one time. -- -- Later, reset to include back inactive clients to the set. -- ClientSet = SET_CLIENT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() -- ... logic ... -- ClientSet = SET_CLIENT:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce() - -- + -- function SET_CLIENT:FilterActive( Active ) Active = Active or not ( Active == false ) self.Filter.Active = Active return self end - - - + + + --- Starts the filtering. -- @param #SET_CLIENT self -- @return #SET_CLIENT self function SET_CLIENT:FilterStart() - + if _DATABASE then self:_FilterStart() self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) end - + return self end - + --- Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! -- @param #SET_CLIENT self @@ -3565,10 +3565,10 @@ do -- SET_CLIENT -- @return #table The CLIENT function SET_CLIENT:AddInDatabase( Event ) self:F3( { Event } ) - + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - + --- Handles the Database to check on any event that Object exists in the Database. -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! -- @param #SET_CLIENT self @@ -3577,22 +3577,22 @@ do -- SET_CLIENT -- @return #table The CLIENT function SET_CLIENT:FindInDatabase( Event ) self:F3( { Event } ) - + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - + --- Iterate the SET_CLIENT and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. -- @param #SET_CLIENT self -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. -- @return #SET_CLIENT self function SET_CLIENT:ForEachClient( IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet() ) - + return self end - + --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence completely in a @{Zone}, providing the CLIENT and optional parameters to the called function. -- @param #SET_CLIENT self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -3600,7 +3600,7 @@ do -- SET_CLIENT -- @return #SET_CLIENT self function SET_CLIENT:ForEachClientInZone( ZoneObject, IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet(), --- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Client#CLIENT ClientObject @@ -3611,10 +3611,10 @@ do -- SET_CLIENT return false end end, { ZoneObject } ) - + return self end - + --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. -- @param #SET_CLIENT self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -3622,7 +3622,7 @@ do -- SET_CLIENT -- @return #SET_CLIENT self function SET_CLIENT:ForEachClientNotInZone( ZoneObject, IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet(), --- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Client#CLIENT ClientObject @@ -3633,22 +3633,22 @@ do -- SET_CLIENT return false end end, { ZoneObject } ) - + return self end - + --- -- @param #SET_CLIENT self -- @param Wrapper.Client#CLIENT MClient -- @return #SET_CLIENT self function SET_CLIENT:IsIncludeObject( MClient ) self:F2( MClient ) - + local MClientInclude = true - + if MClient then local MClientName = MClient.UnitName - + if self.Filter.Active ~= nil then local MClientActive = false if self.Filter.Active == false or ( self.Filter.Active == true and MClient:IsActive() == true ) then @@ -3656,7 +3656,7 @@ do -- SET_CLIENT end MClientInclude = MClientInclude and MClientActive end - + if self.Filter.Coalitions then local MClientCoalition = false for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do @@ -3669,7 +3669,7 @@ do -- SET_CLIENT self:T( { "Evaluated Coalition", MClientCoalition } ) MClientInclude = MClientInclude and MClientCoalition end - + if self.Filter.Categories then local MClientCategory = false for CategoryID, CategoryName in pairs( self.Filter.Categories ) do @@ -3682,7 +3682,7 @@ do -- SET_CLIENT self:T( { "Evaluated Category", MClientCategory } ) MClientInclude = MClientInclude and MClientCategory end - + if self.Filter.Types then local MClientType = false for TypeID, TypeName in pairs( self.Filter.Types ) do @@ -3694,7 +3694,7 @@ do -- SET_CLIENT self:T( { "Evaluated Type", MClientType } ) MClientInclude = MClientInclude and MClientType end - + if self.Filter.Countries then local MClientCountry = false for CountryID, CountryName in pairs( self.Filter.Countries ) do @@ -3707,7 +3707,7 @@ do -- SET_CLIENT self:T( { "Evaluated Country", MClientCountry } ) MClientInclude = MClientInclude and MClientCountry end - + if self.Filter.ClientPrefixes then local MClientPrefix = false for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do @@ -3720,7 +3720,7 @@ do -- SET_CLIENT MClientInclude = MClientInclude and MClientPrefix end end - + self:T2( MClientInclude ) return MClientInclude end @@ -3732,46 +3732,46 @@ do -- SET_PLAYER --- @type SET_PLAYER -- @extends Core.Set#SET_BASE - - - + + + --- Mission designers can use the @{Core.Set#SET_PLAYER} class to build sets of units belonging to alive players: - -- + -- -- ## SET_PLAYER constructor - -- + -- -- Create a new SET_PLAYER object with the @{#SET_PLAYER.New} method: - -- + -- -- * @{#SET_PLAYER.New}: Creates a new SET_PLAYER object. - -- + -- -- ## SET_PLAYER filter criteria - -- + -- -- You can set filter criteria to define the set of clients within the SET_PLAYER. -- Filter criteria are defined by: - -- + -- -- * @{#SET_PLAYER.FilterCoalitions}: Builds the SET_PLAYER with the clients belonging to the coalition(s). -- * @{#SET_PLAYER.FilterCategories}: Builds the SET_PLAYER with the clients belonging to the category(ies). -- * @{#SET_PLAYER.FilterTypes}: Builds the SET_PLAYER with the clients belonging to the client type(s). -- * @{#SET_PLAYER.FilterCountries}: Builds the SET_PLAYER with the clients belonging to the country(ies). -- * @{#SET_PLAYER.FilterPrefixes}: Builds the SET_PLAYER with the clients starting with the same prefix string(s). - -- + -- -- Once the filter criteria have been set for the SET_PLAYER, you can start filtering using: - -- + -- -- * @{#SET_PLAYER.FilterStart}: Starts the filtering of the clients within the SET_PLAYER. - -- + -- -- Planned filter criteria within development are (so these are not yet available): - -- + -- -- * @{#SET_PLAYER.FilterZones}: Builds the SET_PLAYER with the clients within a @{Core.Zone#ZONE}. - -- + -- -- ## SET_PLAYER iterators - -- + -- -- Once the filters have been defined and the SET_PLAYER has been built, you can iterate the SET_PLAYER with the available iterator methods. -- The iterator methods will walk the SET_PLAYER set, and call for each element within the set a function that you provide. -- The following iterator methods are currently available within the SET_PLAYER: - -- + -- -- * @{#SET_PLAYER.ForEachClient}: Calls a function for each alive client it finds within the SET_PLAYER. - -- + -- -- === - -- @field #SET_PLAYER SET_PLAYER + -- @field #SET_PLAYER SET_PLAYER SET_PLAYER = { ClassName = "SET_PLAYER", Clients = {}, @@ -3797,8 +3797,8 @@ do -- SET_PLAYER }, }, } - - + + --- Creates a new SET_PLAYER object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_PLAYER self -- @return #SET_PLAYER @@ -3808,53 +3808,53 @@ do -- SET_PLAYER function SET_PLAYER:New() -- Inherits from BASE local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.PLAYERS ) ) - + return self end - + --- Add CLIENT(s) to SET_PLAYER. -- @param Core.Set#SET_PLAYER self -- @param #string AddClientNames A single name or an array of CLIENT names. -- @return self function SET_PLAYER:AddClientsByName( AddClientNames ) - + local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } - + for AddClientID, AddClientName in pairs( AddClientNamesArray ) do self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) end - + return self end - + --- Remove CLIENT(s) from SET_PLAYER. -- @param Core.Set#SET_PLAYER self -- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. -- @return self function SET_PLAYER:RemoveClientsByName( RemoveClientNames ) - + local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } - + for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do self:Remove( RemoveClientName.ClientName ) end - + return self end - - + + --- Finds a Client based on the Player Name. -- @param #SET_PLAYER self -- @param #string PlayerName -- @return Wrapper.Client#CLIENT The found Client. function SET_PLAYER:FindClient( PlayerName ) - + local ClientFound = self.Set[PlayerName] return ClientFound end - - - + + + --- Builds a set of clients of coalitions joined by specific players. -- Possible current coalitions are red, blue and neutral. -- @param #SET_PLAYER self @@ -3872,8 +3872,8 @@ do -- SET_PLAYER end return self end - - + + --- Builds a set of clients out of categories joined by players. -- Possible current categories are plane, helicopter, ground, ship. -- @param #SET_PLAYER self @@ -3891,8 +3891,8 @@ do -- SET_PLAYER end return self end - - + + --- Builds a set of clients of defined client types joined by players. -- Possible current types are those types known within DCS world. -- @param #SET_PLAYER self @@ -3910,8 +3910,8 @@ do -- SET_PLAYER end return self end - - + + --- Builds a set of clients of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_PLAYER self @@ -3929,8 +3929,8 @@ do -- SET_PLAYER end return self end - - + + --- Builds a set of clients of defined client prefixes. -- All the clients starting with the given prefixes will be included within the set. -- @param #SET_PLAYER self @@ -3948,25 +3948,25 @@ do -- SET_PLAYER end return self end - - - - + + + + --- Starts the filtering. -- @param #SET_PLAYER self -- @return #SET_PLAYER self function SET_PLAYER:FilterStart() - + if _DATABASE then self:_FilterStart() self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) end - + return self end - + --- Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! -- @param #SET_PLAYER self @@ -3975,10 +3975,10 @@ do -- SET_PLAYER -- @return #table The CLIENT function SET_PLAYER:AddInDatabase( Event ) self:F3( { Event } ) - + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - + --- Handles the Database to check on any event that Object exists in the Database. -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! -- @param #SET_PLAYER self @@ -3987,22 +3987,22 @@ do -- SET_PLAYER -- @return #table The CLIENT function SET_PLAYER:FindInDatabase( Event ) self:F3( { Event } ) - + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - + --- Iterate the SET_PLAYER and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. -- @param #SET_PLAYER self -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter. -- @return #SET_PLAYER self function SET_PLAYER:ForEachPlayer( IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet() ) - + return self end - + --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence completely in a @{Zone}, providing the CLIENT and optional parameters to the called function. -- @param #SET_PLAYER self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -4010,7 +4010,7 @@ do -- SET_PLAYER -- @return #SET_PLAYER self function SET_PLAYER:ForEachPlayerInZone( ZoneObject, IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet(), --- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Client#CLIENT ClientObject @@ -4021,10 +4021,10 @@ do -- SET_PLAYER return false end end, { ZoneObject } ) - + return self end - + --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. -- @param #SET_PLAYER self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -4032,7 +4032,7 @@ do -- SET_PLAYER -- @return #SET_PLAYER self function SET_PLAYER:ForEachPlayerNotInZone( ZoneObject, IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet(), --- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Client#CLIENT ClientObject @@ -4043,22 +4043,22 @@ do -- SET_PLAYER return false end end, { ZoneObject } ) - + return self end - + --- -- @param #SET_PLAYER self -- @param Wrapper.Client#CLIENT MClient -- @return #SET_PLAYER self function SET_PLAYER:IsIncludeObject( MClient ) self:F2( MClient ) - + local MClientInclude = true - + if MClient then local MClientName = MClient.UnitName - + if self.Filter.Coalitions then local MClientCoalition = false for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do @@ -4071,7 +4071,7 @@ do -- SET_PLAYER self:T( { "Evaluated Coalition", MClientCoalition } ) MClientInclude = MClientInclude and MClientCoalition end - + if self.Filter.Categories then local MClientCategory = false for CategoryID, CategoryName in pairs( self.Filter.Categories ) do @@ -4084,7 +4084,7 @@ do -- SET_PLAYER self:T( { "Evaluated Category", MClientCategory } ) MClientInclude = MClientInclude and MClientCategory end - + if self.Filter.Types then local MClientType = false for TypeID, TypeName in pairs( self.Filter.Types ) do @@ -4096,7 +4096,7 @@ do -- SET_PLAYER self:T( { "Evaluated Type", MClientType } ) MClientInclude = MClientInclude and MClientType end - + if self.Filter.Countries then local MClientCountry = false for CountryID, CountryName in pairs( self.Filter.Countries ) do @@ -4109,7 +4109,7 @@ do -- SET_PLAYER self:T( { "Evaluated Country", MClientCountry } ) MClientInclude = MClientInclude and MClientCountry end - + if self.Filter.ClientPrefixes then local MClientPrefix = false for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do @@ -4122,7 +4122,7 @@ do -- SET_PLAYER MClientInclude = MClientInclude and MClientPrefix end end - + self:T2( MClientInclude ) return MClientInclude end @@ -4134,41 +4134,41 @@ do -- SET_AIRBASE --- @type SET_AIRBASE -- @extends Core.Set#SET_BASE - + --- Mission designers can use the @{Core.Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain: - -- + -- -- * Coalitions - -- + -- -- ## SET_AIRBASE constructor - -- + -- -- Create a new SET_AIRBASE object with the @{#SET_AIRBASE.New} method: - -- + -- -- * @{#SET_AIRBASE.New}: Creates a new SET_AIRBASE object. - -- - -- ## Add or Remove AIRBASEs from SET_AIRBASE - -- - -- AIRBASEs can be added and removed using the @{Core.Set#SET_AIRBASE.AddAirbasesByName} and @{Core.Set#SET_AIRBASE.RemoveAirbasesByName} respectively. + -- + -- ## Add or Remove AIRBASEs from SET_AIRBASE + -- + -- AIRBASEs can be added and removed using the @{Core.Set#SET_AIRBASE.AddAirbasesByName} and @{Core.Set#SET_AIRBASE.RemoveAirbasesByName} respectively. -- These methods take a single AIRBASE name or an array of AIRBASE names to be added or removed from SET_AIRBASE. - -- - -- ## SET_AIRBASE filter criteria - -- + -- + -- ## SET_AIRBASE filter criteria + -- -- You can set filter criteria to define the set of clients within the SET_AIRBASE. -- Filter criteria are defined by: - -- + -- -- * @{#SET_AIRBASE.FilterCoalitions}: Builds the SET_AIRBASE with the airbases belonging to the coalition(s). - -- + -- -- Once the filter criteria have been set for the SET_AIRBASE, you can start filtering using: - -- + -- -- * @{#SET_AIRBASE.FilterStart}: Starts the filtering of the airbases within the SET_AIRBASE. - -- + -- -- ## SET_AIRBASE iterators - -- + -- -- Once the filters have been defined and the SET_AIRBASE has been built, you can iterate the SET_AIRBASE with the available iterator methods. -- The iterator methods will walk the SET_AIRBASE set, and call for each airbase within the set a function that you provide. -- The following iterator methods are currently available within the SET_AIRBASE: - -- + -- -- * @{#SET_AIRBASE.ForEachAirbase}: Calls a function for each airbase it finds within the SET_AIRBASE. - -- + -- -- === -- @field #SET_AIRBASE SET_AIRBASE SET_AIRBASE = { @@ -4190,8 +4190,8 @@ do -- SET_AIRBASE }, }, } - - + + --- Creates a new SET_AIRBASE object, building a set of airbases belonging to a coalitions and categories. -- @param #SET_AIRBASE self -- @return #SET_AIRBASE self @@ -4201,103 +4201,103 @@ do -- SET_AIRBASE function SET_AIRBASE:New() -- Inherits from BASE local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.AIRBASES ) ) - + return self end - + --- Add an AIRBASE object to SET_AIRBASE. -- @param Core.Set#SET_AIRBASE self -- @param Wrapper.Airbase#AIRBASE airbase Airbase that should be added to the set. -- @return self function SET_AIRBASE:AddAirbase( airbase ) - + self:Add( airbase:GetName(), airbase ) - + return self end - + --- Add AIRBASEs to SET_AIRBASE. -- @param Core.Set#SET_AIRBASE self -- @param #string AddAirbaseNames A single name or an array of AIRBASE names. -- @return self function SET_AIRBASE:AddAirbasesByName( AddAirbaseNames ) - + local AddAirbaseNamesArray = ( type( AddAirbaseNames ) == "table" ) and AddAirbaseNames or { AddAirbaseNames } - + for AddAirbaseID, AddAirbaseName in pairs( AddAirbaseNamesArray ) do self:Add( AddAirbaseName, AIRBASE:FindByName( AddAirbaseName ) ) end - + return self end - + --- Remove AIRBASEs from SET_AIRBASE. -- @param Core.Set#SET_AIRBASE self -- @param Wrapper.Airbase#AIRBASE RemoveAirbaseNames A single name or an array of AIRBASE names. -- @return self function SET_AIRBASE:RemoveAirbasesByName( RemoveAirbaseNames ) - + local RemoveAirbaseNamesArray = ( type( RemoveAirbaseNames ) == "table" ) and RemoveAirbaseNames or { RemoveAirbaseNames } - + for RemoveAirbaseID, RemoveAirbaseName in pairs( RemoveAirbaseNamesArray ) do self:Remove( RemoveAirbaseName ) end - + return self end - - + + --- Finds a Airbase based on the Airbase Name. -- @param #SET_AIRBASE self -- @param #string AirbaseName -- @return Wrapper.Airbase#AIRBASE The found Airbase. function SET_AIRBASE:FindAirbase( AirbaseName ) - + local AirbaseFound = self.Set[AirbaseName] return AirbaseFound end - - + + --- Finds an Airbase in range of a coordinate. -- @param #SET_AIRBASE self -- @param Core.Point#COORDINATE Coordinate -- @param #number Range -- @return Wrapper.Airbase#AIRBASE The found Airbase. function SET_AIRBASE:FindAirbaseInRange( Coordinate, Range ) - + local AirbaseFound = nil - + for AirbaseName, AirbaseObject in pairs( self.Set ) do - + local AirbaseCoordinate = AirbaseObject:GetCoordinate() local Distance = Coordinate:Get2DDistance( AirbaseCoordinate ) - + self:F({Distance=Distance}) - + if Distance <= Range then AirbaseFound = AirbaseObject break end - + end - + return AirbaseFound end - - + + --- Finds a random Airbase in the set. -- @param #SET_AIRBASE self -- @return Wrapper.Airbase#AIRBASE The found Airbase. function SET_AIRBASE:GetRandomAirbase() - + local RandomAirbase = self:GetRandom() self:F( { RandomAirbase = RandomAirbase:GetName() } ) - + return RandomAirbase end - - - + + + --- Builds a set of airbases of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_AIRBASE self @@ -4315,8 +4315,8 @@ do -- SET_AIRBASE end return self end - - + + --- Builds a set of airbases out of categories. -- Possible current categories are plane, helicopter, ground, ship. -- @param #SET_AIRBASE self @@ -4334,17 +4334,17 @@ do -- SET_AIRBASE end return self end - + --- Starts the filtering. -- @param #SET_AIRBASE self -- @return #SET_AIRBASE self function SET_AIRBASE:FilterStart() - + if _DATABASE then - + -- We use the BaseCaptured event, which is generated by DCS when a base got captured. self:HandleEvent( EVENTS.BaseCaptured ) - + -- We initialize the first set. for ObjectName, Object in pairs( self.Database ) do if self:IsIncludeObject( Object ) then @@ -4354,16 +4354,16 @@ do -- SET_AIRBASE end end end - + return self end - + --- Starts the filtering. -- @param #SET_AIRBASE self -- @param Core.Event#EVENT EventData -- @return #SET_AIRBASE self function SET_AIRBASE:OnEventBaseCaptured(EventData) - + -- When a base got captured, we reevaluate the set. for ObjectName, Object in pairs( self.Database ) do if self:IsIncludeObject( Object ) then @@ -4374,9 +4374,9 @@ do -- SET_AIRBASE self:RemoveAirbasesByName( ObjectName ) end end - + end - + --- Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! -- @param #SET_AIRBASE self @@ -4385,10 +4385,10 @@ do -- SET_AIRBASE -- @return #table The AIRBASE function SET_AIRBASE:AddInDatabase( Event ) self:F3( { Event } ) - + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - + --- Handles the Database to check on any event that Object exists in the Database. -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! -- @param #SET_AIRBASE self @@ -4397,47 +4397,47 @@ do -- SET_AIRBASE -- @return #table The AIRBASE function SET_AIRBASE:FindInDatabase( Event ) self:F3( { Event } ) - + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - + --- Iterate the SET_AIRBASE and call an interator function for each AIRBASE, providing the AIRBASE and optional parameters. -- @param #SET_AIRBASE self -- @param #function IteratorFunction The function that will be called when there is an alive AIRBASE in the SET_AIRBASE. The function needs to accept a AIRBASE parameter. -- @return #SET_AIRBASE self function SET_AIRBASE:ForEachAirbase( IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet() ) - + return self end - + --- Iterate the SET_AIRBASE while identifying the nearest @{Wrapper.Airbase#AIRBASE} from a @{Core.Point#POINT_VEC2}. -- @param #SET_AIRBASE self -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest @{Wrapper.Airbase#AIRBASE}. -- @return Wrapper.Airbase#AIRBASE The closest @{Wrapper.Airbase#AIRBASE}. function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 ) self:F2( PointVec2 ) - + local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 ) return NearestAirbase end - - - + + + --- -- @param #SET_AIRBASE self -- @param Wrapper.Airbase#AIRBASE MAirbase -- @return #SET_AIRBASE self function SET_AIRBASE:IsIncludeObject( MAirbase ) self:F2( MAirbase ) - + local MAirbaseInclude = true - + if MAirbase then local MAirbaseName = MAirbase:GetName() - + if self.Filter.Coalitions then local MAirbaseCoalition = false for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do @@ -4450,7 +4450,7 @@ do -- SET_AIRBASE self:T( { "Evaluated Coalition", MAirbaseCoalition } ) MAirbaseInclude = MAirbaseInclude and MAirbaseCoalition end - + if self.Filter.Categories then local MAirbaseCategory = false for CategoryID, CategoryName in pairs( self.Filter.Categories ) do @@ -4464,7 +4464,7 @@ do -- SET_AIRBASE MAirbaseInclude = MAirbaseInclude and MAirbaseCategory end end - + self:T2( MAirbaseInclude ) return MAirbaseInclude end @@ -4476,48 +4476,48 @@ do -- SET_CARGO --- @type SET_CARGO -- @extends Core.Set#SET_BASE - + --- Mission designers can use the @{Core.Set#SET_CARGO} class to build sets of cargos optionally belonging to certain: - -- + -- -- * Coalitions -- * Types -- * Name or Prefix - -- + -- -- ## SET_CARGO constructor - -- + -- -- Create a new SET_CARGO object with the @{#SET_CARGO.New} method: - -- + -- -- * @{#SET_CARGO.New}: Creates a new SET_CARGO object. - -- - -- ## Add or Remove CARGOs from SET_CARGO - -- - -- CARGOs can be added and removed using the @{Core.Set#SET_CARGO.AddCargosByName} and @{Core.Set#SET_CARGO.RemoveCargosByName} respectively. + -- + -- ## Add or Remove CARGOs from SET_CARGO + -- + -- CARGOs can be added and removed using the @{Core.Set#SET_CARGO.AddCargosByName} and @{Core.Set#SET_CARGO.RemoveCargosByName} respectively. -- These methods take a single CARGO name or an array of CARGO names to be added or removed from SET_CARGO. - -- - -- ## SET_CARGO filter criteria - -- + -- + -- ## SET_CARGO filter criteria + -- -- You can set filter criteria to automatically maintain the SET_CARGO contents. -- Filter criteria are defined by: - -- + -- -- * @{#SET_CARGO.FilterCoalitions}: Builds the SET_CARGO with the cargos belonging to the coalition(s). -- * @{#SET_CARGO.FilterPrefixes}: Builds the SET_CARGO with the cargos containing the prefix string(s). -- * @{#SET_CARGO.FilterTypes}: Builds the SET_CARGO with the cargos belonging to the cargo type(s). -- * @{#SET_CARGO.FilterCountries}: Builds the SET_CARGO with the cargos belonging to the country(ies). - -- + -- -- Once the filter criteria have been set for the SET_CARGO, you can start filtering using: - -- + -- -- * @{#SET_CARGO.FilterStart}: Starts the filtering of the cargos within the SET_CARGO. - -- + -- -- ## SET_CARGO iterators - -- + -- -- Once the filters have been defined and the SET_CARGO has been built, you can iterate the SET_CARGO with the available iterator methods. -- The iterator methods will walk the SET_CARGO set, and call for each cargo within the set a function that you provide. -- The following iterator methods are currently available within the SET_CARGO: - -- + -- -- * @{#SET_CARGO.ForEachCargo}: Calls a function for each cargo it finds within the SET_CARGO. - -- + -- -- @field #SET_CARGO SET_CARGO - -- + -- SET_CARGO = { ClassName = "SET_CARGO", Cargos = {}, @@ -4535,8 +4535,8 @@ do -- SET_CARGO }, }, } - - + + --- Creates a new SET_CARGO object, building a set of cargos belonging to a coalitions and categories. -- @param #SET_CARGO self -- @return #SET_CARGO @@ -4546,66 +4546,66 @@ do -- SET_CARGO function SET_CARGO:New() --R2.1 -- Inherits from BASE local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CARGOS ) ) -- #SET_CARGO - + return self end - - + + --- (R2.1) Add CARGO to SET_CARGO. -- @param Core.Set#SET_CARGO self -- @param Cargo.Cargo#CARGO Cargo A single cargo. -- @return self function SET_CARGO:AddCargo( Cargo ) --R2.4 - + self:Add( Cargo:GetName(), Cargo ) - + return self end - - + + --- (R2.1) Add CARGOs to SET_CARGO. -- @param Core.Set#SET_CARGO self -- @param #string AddCargoNames A single name or an array of CARGO names. -- @return self function SET_CARGO:AddCargosByName( AddCargoNames ) --R2.1 - + local AddCargoNamesArray = ( type( AddCargoNames ) == "table" ) and AddCargoNames or { AddCargoNames } - + for AddCargoID, AddCargoName in pairs( AddCargoNamesArray ) do self:Add( AddCargoName, CARGO:FindByName( AddCargoName ) ) end - + return self end - + --- (R2.1) Remove CARGOs from SET_CARGO. -- @param Core.Set#SET_CARGO self -- @param Wrapper.Cargo#CARGO RemoveCargoNames A single name or an array of CARGO names. -- @return self function SET_CARGO:RemoveCargosByName( RemoveCargoNames ) --R2.1 - + local RemoveCargoNamesArray = ( type( RemoveCargoNames ) == "table" ) and RemoveCargoNames or { RemoveCargoNames } - + for RemoveCargoID, RemoveCargoName in pairs( RemoveCargoNamesArray ) do self:Remove( RemoveCargoName.CargoName ) end - + return self end - - + + --- (R2.1) Finds a Cargo based on the Cargo Name. -- @param #SET_CARGO self -- @param #string CargoName -- @return Wrapper.Cargo#CARGO The found Cargo. function SET_CARGO:FindCargo( CargoName ) --R2.1 - + local CargoFound = self.Set[CargoName] return CargoFound end - - - + + + --- (R2.1) Builds a set of cargos of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_CARGO self @@ -4623,7 +4623,7 @@ do -- SET_CARGO end return self end - + --- (R2.1) Builds a set of cargos of defined cargo types. -- Possible current types are those types known within DCS world. -- @param #SET_CARGO self @@ -4641,8 +4641,8 @@ do -- SET_CARGO end return self end - - + + --- (R2.1) Builds a set of cargos of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_CARGO self @@ -4660,8 +4660,8 @@ do -- SET_CARGO end return self end - - + + --- (R2.1) Builds a set of cargos of defined cargo prefixes. -- All the cargos starting with the given prefixes will be included within the set. -- @param #SET_CARGO self @@ -4679,35 +4679,35 @@ do -- SET_CARGO end return self end - - - + + + --- (R2.1) Starts the filtering. -- @param #SET_CARGO self -- @return #SET_CARGO self function SET_CARGO:FilterStart() --R2.1 - + if _DATABASE then self:_FilterStart() self:HandleEvent( EVENTS.NewCargo ) self:HandleEvent( EVENTS.DeleteCargo ) end - + return self end - + --- Stops the filtering for the defined collection. -- @param #SET_CARGO self -- @return #SET_CARGO self function SET_CARGO:FilterStop() - + self:UnHandleEvent( EVENTS.NewCargo ) self:UnHandleEvent( EVENTS.DeleteCargo ) - + return self end - - + + --- (R2.1) Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! -- @param #SET_CARGO self @@ -4716,10 +4716,10 @@ do -- SET_CARGO -- @return #table The CARGO function SET_CARGO:AddInDatabase( Event ) --R2.1 self:F3( { Event } ) - + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - + --- (R2.1) Handles the Database to check on any event that Object exists in the Database. -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! -- @param #SET_CARGO self @@ -4728,62 +4728,62 @@ do -- SET_CARGO -- @return #table The CARGO function SET_CARGO:FindInDatabase( Event ) --R2.1 self:F3( { Event } ) - + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - + --- (R2.1) Iterate the SET_CARGO and call an interator function for each CARGO, providing the CARGO and optional parameters. -- @param #SET_CARGO self -- @param #function IteratorFunction The function that will be called when there is an alive CARGO in the SET_CARGO. The function needs to accept a CARGO parameter. -- @return #SET_CARGO self function SET_CARGO:ForEachCargo( IteratorFunction, ... ) --R2.1 self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet() ) - + return self end - + --- (R2.1) Iterate the SET_CARGO while identifying the nearest @{Cargo.Cargo#CARGO} from a @{Core.Point#POINT_VEC2}. -- @param #SET_CARGO self -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest @{Cargo.Cargo#CARGO}. -- @return Wrapper.Cargo#CARGO The closest @{Cargo.Cargo#CARGO}. function SET_CARGO:FindNearestCargoFromPointVec2( PointVec2 ) --R2.1 self:F2( PointVec2 ) - + local NearestCargo = self:FindNearestObjectFromPointVec2( PointVec2 ) return NearestCargo end - + function SET_CARGO:FirstCargoWithState( State ) - + local FirstCargo = nil - + for CargoName, Cargo in pairs( self.Set ) do if Cargo:Is( State ) then FirstCargo = Cargo break end end - + return FirstCargo end - + function SET_CARGO:FirstCargoWithStateAndNotDeployed( State ) - + local FirstCargo = nil - + for CargoName, Cargo in pairs( self.Set ) do if Cargo:Is( State ) and not Cargo:IsDeployed() then FirstCargo = Cargo break end end - + return FirstCargo end - - + + --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is UnLoaded. -- @param #SET_CARGO self -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. @@ -4791,8 +4791,8 @@ do -- SET_CARGO local FirstCargo = self:FirstCargoWithState( "UnLoaded" ) return FirstCargo end - - + + --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is UnLoaded and not Deployed. -- @param #SET_CARGO self -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. @@ -4800,8 +4800,8 @@ do -- SET_CARGO local FirstCargo = self:FirstCargoWithStateAndNotDeployed( "UnLoaded" ) return FirstCargo end - - + + --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is Loaded. -- @param #SET_CARGO self -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. @@ -4809,8 +4809,8 @@ do -- SET_CARGO local FirstCargo = self:FirstCargoWithState( "Loaded" ) return FirstCargo end - - + + --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is Deployed. -- @param #SET_CARGO self -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. @@ -4818,22 +4818,22 @@ do -- SET_CARGO local FirstCargo = self:FirstCargoWithState( "Deployed" ) return FirstCargo end - - - - - --- (R2.1) + + + + + --- (R2.1) -- @param #SET_CARGO self -- @param AI.AI_Cargo#AI_CARGO MCargo -- @return #SET_CARGO self function SET_CARGO:IsIncludeObject( MCargo ) --R2.1 self:F2( MCargo ) - + local MCargoInclude = true - + if MCargo then local MCargoName = MCargo:GetName() - + if self.Filter.Coalitions then local MCargoCoalition = false for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do @@ -4846,7 +4846,7 @@ do -- SET_CARGO self:F( { "Evaluated Coalition", MCargoCoalition } ) MCargoInclude = MCargoInclude and MCargoCoalition end - + if self.Filter.Types then local MCargoType = false for TypeID, TypeName in pairs( self.Filter.Types ) do @@ -4858,7 +4858,7 @@ do -- SET_CARGO self:F( { "Evaluated Type", MCargoType } ) MCargoInclude = MCargoInclude and MCargoType end - + if self.Filter.CargoPrefixes then local MCargoPrefix = false for CargoPrefixId, CargoPrefix in pairs( self.Filter.CargoPrefixes ) do @@ -4871,35 +4871,35 @@ do -- SET_CARGO MCargoInclude = MCargoInclude and MCargoPrefix end end - + self:T2( MCargoInclude ) return MCargoInclude end - + --- (R2.1) Handles the OnEventNewCargo event for the Set. -- @param #SET_CARGO self -- @param Core.Event#EVENTDATA EventData function SET_CARGO:OnEventNewCargo( EventData ) --R2.1 - + self:F( { "New Cargo", EventData } ) - + if EventData.Cargo then if EventData.Cargo and self:IsIncludeObject( EventData.Cargo ) then self:Add( EventData.Cargo.Name , EventData.Cargo ) end end end - + --- (R2.1) Handles the OnDead or OnCrash event for alive units set. -- @param #SET_CARGO self -- @param Core.Event#EVENTDATA EventData function SET_CARGO:OnEventDeleteCargo( EventData ) --R2.1 self:F3( { EventData } ) - + if EventData.Cargo then local Cargo = _DATABASE:FindCargo( EventData.Cargo.Name ) if Cargo and Cargo.Name then - + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. -- And this is a problem because it will remove all entries from the SET_CARGOs. @@ -4922,39 +4922,39 @@ do -- SET_ZONE --- @type SET_ZONE -- @extends Core.Set#SET_BASE - + --- Mission designers can use the @{Core.Set#SET_ZONE} class to build sets of zones of various types. - -- + -- -- ## SET_ZONE constructor - -- + -- -- Create a new SET_ZONE object with the @{#SET_ZONE.New} method: - -- + -- -- * @{#SET_ZONE.New}: Creates a new SET_ZONE object. - -- - -- ## Add or Remove ZONEs from SET_ZONE - -- - -- ZONEs can be added and removed using the @{Core.Set#SET_ZONE.AddZonesByName} and @{Core.Set#SET_ZONE.RemoveZonesByName} respectively. + -- + -- ## Add or Remove ZONEs from SET_ZONE + -- + -- ZONEs can be added and removed using the @{Core.Set#SET_ZONE.AddZonesByName} and @{Core.Set#SET_ZONE.RemoveZonesByName} respectively. -- These methods take a single ZONE name or an array of ZONE names to be added or removed from SET_ZONE. - -- - -- ## SET_ZONE filter criteria - -- + -- + -- ## SET_ZONE filter criteria + -- -- You can set filter criteria to build the collection of zones in SET_ZONE. -- Filter criteria are defined by: - -- + -- -- * @{#SET_ZONE.FilterPrefixes}: Builds the SET_ZONE with the zones having a certain text pattern of prefix. - -- + -- -- Once the filter criteria have been set for the SET_ZONE, you can start filtering using: - -- + -- -- * @{#SET_ZONE.FilterStart}: Starts the filtering of the zones within the SET_ZONE. - -- + -- -- ## SET_ZONE iterators - -- + -- -- Once the filters have been defined and the SET_ZONE has been built, you can iterate the SET_ZONE with the available iterator methods. -- The iterator methods will walk the SET_ZONE set, and call for each airbase within the set a function that you provide. -- The following iterator methods are currently available within the SET_ZONE: - -- + -- -- * @{#SET_ZONE.ForEachZone}: Calls a function for each zone it finds within the SET_ZONE. - -- + -- -- === -- @field #SET_ZONE SET_ZONE SET_ZONE = { @@ -4966,8 +4966,8 @@ do -- SET_ZONE FilterMeta = { }, } - - + + --- Creates a new SET_ZONE object, building a set of zones. -- @param #SET_ZONE self -- @return #SET_ZONE self @@ -4977,90 +4977,90 @@ do -- SET_ZONE function SET_ZONE:New() -- Inherits from BASE local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.ZONES ) ) - + return self end - + --- Add ZONEs by a search name to SET_ZONE. -- @param Core.Set#SET_ZONE self -- @param #string AddZoneNames A single name or an array of ZONE_BASE names. -- @return self function SET_ZONE:AddZonesByName( AddZoneNames ) - + local AddZoneNamesArray = ( type( AddZoneNames ) == "table" ) and AddZoneNames or { AddZoneNames } - + for AddAirbaseID, AddZoneName in pairs( AddZoneNamesArray ) do self:Add( AddZoneName, ZONE:FindByName( AddZoneName ) ) end - + return self end - + --- Add ZONEs to SET_ZONE. -- @param Core.Set#SET_ZONE self -- @param Core.Zone#ZONE_BASE Zone A ZONE_BASE object. -- @return self function SET_ZONE:AddZone( Zone ) - + self:Add( Zone:GetName(), Zone ) - + return self end - - + + --- Remove ZONEs from SET_ZONE. -- @param Core.Set#SET_ZONE self -- @param Core.Zone#ZONE_BASE RemoveZoneNames A single name or an array of ZONE_BASE names. -- @return self function SET_ZONE:RemoveZonesByName( RemoveZoneNames ) - + local RemoveZoneNamesArray = ( type( RemoveZoneNames ) == "table" ) and RemoveZoneNames or { RemoveZoneNames } - + for RemoveZoneID, RemoveZoneName in pairs( RemoveZoneNamesArray ) do self:Remove( RemoveZoneName ) end - + return self end - - + + --- Finds a Zone based on the Zone Name. -- @param #SET_ZONE self -- @param #string ZoneName -- @return Core.Zone#ZONE_BASE The found Zone. function SET_ZONE:FindZone( ZoneName ) - + local ZoneFound = self.Set[ZoneName] return ZoneFound end - - + + --- Get a random zone from the set. -- @param #SET_ZONE self -- @return Core.Zone#ZONE_BASE The random Zone. -- @return #nil if no zone in the collection. function SET_ZONE:GetRandomZone() - + if self:Count() ~= 0 then - + local Index = self.Index local ZoneFound = nil -- Core.Zone#ZONE_BASE - + -- Loop until a zone has been found. -- The :GetZoneMaybe() call will evaluate the probability for the zone to be selected. - -- If the zone is not selected, then nil is returned by :GetZoneMaybe() and the loop continues! + -- If the zone is not selected, then nil is returned by :GetZoneMaybe() and the loop continues! while not ZoneFound do local ZoneRandom = math.random( 1, #Index ) - ZoneFound = self.Set[Index[ZoneRandom]]:GetZoneMaybe() + ZoneFound = self.Set[Index[ZoneRandom]]:GetZoneMaybe() end - + return ZoneFound end - + return nil end - - + + --- Set a zone probability. -- @param #SET_ZONE self -- @param #string ZoneName The name of the zone. @@ -5068,10 +5068,10 @@ do -- SET_ZONE local Zone = self:FindZone( ZoneName ) Zone:SetZoneProbability( ZoneProbability ) end - - - - + + + + --- Builds a set of zones of defined zone prefixes. -- All the zones starting with the given prefixes will be included within the set. -- @param #SET_ZONE self @@ -5089,15 +5089,15 @@ do -- SET_ZONE end return self end - - + + --- Starts the filtering. -- @param #SET_ZONE self -- @return #SET_ZONE self function SET_ZONE:FilterStart() - + if _DATABASE then - + -- We initialize the first set. for ObjectName, Object in pairs( self.Database ) do if self:IsIncludeObject( Object ) then @@ -5107,24 +5107,24 @@ do -- SET_ZONE end end end - + self:HandleEvent( EVENTS.NewZone ) self:HandleEvent( EVENTS.DeleteZone ) - + return self end - + --- Stops the filtering for the defined collection. -- @param #SET_ZONE self -- @return #SET_ZONE self function SET_ZONE:FilterStop() - + self:UnHandleEvent( EVENTS.NewZone ) self:UnHandleEvent( EVENTS.DeleteZone ) - + return self end - + --- Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! -- @param #SET_ZONE self @@ -5133,10 +5133,10 @@ do -- SET_ZONE -- @return #table The AIRBASE function SET_ZONE:AddInDatabase( Event ) self:F3( { Event } ) - + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - + --- Handles the Database to check on any event that Object exists in the Database. -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! -- @param #SET_ZONE self @@ -5145,35 +5145,35 @@ do -- SET_ZONE -- @return #table The AIRBASE function SET_ZONE:FindInDatabase( Event ) self:F3( { Event } ) - + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end - + --- Iterate the SET_ZONE and call an interator function for each ZONE, providing the ZONE and optional parameters. -- @param #SET_ZONE self -- @param #function IteratorFunction The function that will be called when there is an alive ZONE in the SET_ZONE. The function needs to accept a AIRBASE parameter. -- @return #SET_ZONE self function SET_ZONE:ForEachZone( IteratorFunction, ... ) self:F2( arg ) - + self:ForEach( IteratorFunction, arg, self:GetSet() ) - + return self end - - + + --- -- @param #SET_ZONE self -- @param Core.Zone#ZONE_BASE MZone -- @return #SET_ZONE self function SET_ZONE:IsIncludeObject( MZone ) self:F2( MZone ) - + local MZoneInclude = true - + if MZone then local MZoneName = MZone:GetName() - + if self.Filter.Prefixes then local MZonePrefix = false for ZonePrefixId, ZonePrefix in pairs( self.Filter.Prefixes ) do @@ -5186,35 +5186,35 @@ do -- SET_ZONE MZoneInclude = MZoneInclude and MZonePrefix end end - + self:T2( MZoneInclude ) return MZoneInclude end - + --- Handles the OnEventNewZone event for the Set. -- @param #SET_ZONE self -- @param Core.Event#EVENTDATA EventData function SET_ZONE:OnEventNewZone( EventData ) --R2.1 - + self:F( { "New Zone", EventData } ) - + if EventData.Zone then if EventData.Zone and self:IsIncludeObject( EventData.Zone ) then self:Add( EventData.Zone.ZoneName , EventData.Zone ) end end end - + --- Handles the OnDead or OnCrash event for alive units set. -- @param #SET_ZONE self -- @param Core.Event#EVENTDATA EventData function SET_ZONE:OnEventDeleteZone( EventData ) --R2.1 self:F3( { EventData } ) - + if EventData.Zone then local Zone = _DATABASE:FindZone( EventData.Zone.ZoneName ) if Zone and Zone.ZoneName then - + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. -- And this is a problem because it will remove all entries from the SET_ZONEs. @@ -5229,7 +5229,7 @@ do -- SET_ZONE end end end - + --- Validate if a coordinate is in one of the zones in the set. -- Returns the ZONE object where the coordiante is located. -- If zones overlap, the first zone that validates the test is returned. @@ -5238,15 +5238,15 @@ do -- SET_ZONE -- @return Core.Zone#ZONE_BASE The zone that validates the coordinate location. -- @return #nil No zone has been found. function SET_ZONE:IsCoordinateInZone( Coordinate ) - + for _, Zone in pairs( self:GetSet() ) do local Zone = Zone -- Core.Zone#ZONE_BASE if Zone:IsCoordinateInZone( Coordinate ) then return Zone end end - + return nil end -end +end \ No newline at end of file diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 11aaf062d..466e6a70f 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1702,7 +1702,7 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT -- When spawned in the air, we need to generate a Takeoff Event. if Takeoff == GROUP.Takeoff.Air then for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do - SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() } , 1 ) + SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() } , 5 ) end end @@ -2005,10 +2005,10 @@ end function SPAWN:InitUnControlled( UnControlled ) self:F2( { self.SpawnTemplatePrefix, UnControlled } ) - self.SpawnUnControlled = UnControlled + self.SpawnUnControlled = UnControlled or true for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = UnControlled + self.SpawnGroups[SpawnGroupID].UnControlled = self.SpawnUnControlled end return self diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index c1370b2a1..e0971ee06 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -618,6 +618,9 @@ function ZONE_RADIUS:GetVec3( Height ) end + + + --- Scan the zone for the presence of units of the given ObjectCategories. -- Note that after a zone has been scanned, the zone can be evaluated by: -- @@ -629,11 +632,11 @@ end -- @{#ZONE_RADIUS. -- @param #ZONE_RADIUS self -- @param ObjectCategories --- @param Coalition +-- @param UnitCategories -- @usage -- self.Zone:Scan() -- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) -function ZONE_RADIUS:Scan( ObjectCategories ) +function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) self.ScanData = {} self.ScanData.Coalitions = {} @@ -660,9 +663,24 @@ function ZONE_RADIUS:Scan( ObjectCategories ) if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then local CoalitionDCSUnit = ZoneObject:getCoalition() - self.ScanData.Coalitions[CoalitionDCSUnit] = true - self.ScanData.Units[ZoneObject] = ZoneObject - self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) + local Include = false + if not UnitCategories then + Include = true + else + local CategoryDCSUnit = ZoneObject:getDesc().category + for UnitCategoryID, UnitCategory in pairs( UnitCategories ) do + if UnitCategory == CategoryDCSUnit then + Include = true + break + end + end + end + if Include then + local CoalitionDCSUnit = ZoneObject:getCoalition() + self.ScanData.Coalitions[CoalitionDCSUnit] = true + self.ScanData.Units[ZoneObject] = ZoneObject + self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) + end end if ObjectCategory == Object.Category.SCENERY then local SceneryType = ZoneObject:getTypeName() diff --git a/Moose Development/Moose/Functional/Designate.lua b/Moose Development/Moose/Functional/Designate.lua index d917d1cd3..b8b1c24bb 100644 --- a/Moose Development/Moose/Functional/Designate.lua +++ b/Moose Development/Moose/Functional/Designate.lua @@ -286,9 +286,9 @@ do -- DESIGNATE -- * The status report can be automatically flashed by selecting "Status" -> "Flash Status On". -- * The automatic flashing of the status report can be deactivated by selecting "Status" -> "Flash Status Off". -- * The flashing of the status menu is disabled by default. - -- * The method @{#DESIGNATE.FlashStatusMenu}() can be used to enable or disable to flashing of the status menu. + -- * The method @{#DESIGNATE.SetFlashStatusMenu}() can be used to enable or disable to flashing of the status menu. -- - -- Designate:FlashStatusMenu( true ) + -- Designate:SetFlashStatusMenu( true ) -- -- The example will activate the flashing of the status menu for this Designate object. -- @@ -474,7 +474,7 @@ do -- DESIGNATE self.Designating = {} self:SetDesignateName() - self.LaseDuration = 60 + self:SetLaseDuration() -- Default is 120 seconds. self:SetFlashStatusMenu( false ) self:SetFlashDetectionMessages( true ) @@ -677,6 +677,14 @@ do -- DESIGNATE return self end + --- Set the lase duration for designations. + -- @param #DESIGNATE self + -- @param #number LaseDuration The time in seconds a lase will continue to hold on target. The default is 120 seconds. + -- @return #DESIGNATE + function DESIGNATE:SetLaseDuration( LaseDuration ) + self.LaseDuration = LaseDuration or 120 + return self + end --- Generate an array of possible laser codes. -- Each new lase will select a code from this table. @@ -1000,9 +1008,9 @@ do -- DESIGNATE if string.find( Designating, "L", 1, true ) == nil then MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Search other target", DetectedMenu, self.MenuForget, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) for LaserCode, MenuText in pairs( self.MenuLaserCodes ) do - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, string.format( MenuText, LaserCode ), DetectedMenu, self.MenuLaseCode, self, DesignateIndex, 60, LaserCode ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, string.format( MenuText, LaserCode ), DetectedMenu, self.MenuLaseCode, self, DesignateIndex, self.LaseDuration, LaserCode ):SetTime( MenuTime ):SetTag( self.DesignateName ) end - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Lase with random laser code(s)", DetectedMenu, self.MenuLaseOn, self, DesignateIndex, 60 ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Lase with random laser code(s)", DetectedMenu, self.MenuLaseOn, self, DesignateIndex, self.LaseDuration ):SetTime( MenuTime ):SetTag( self.DesignateName ) else MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Stop lasing", DetectedMenu, self.MenuLaseOff, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) end @@ -1160,10 +1168,10 @@ do -- DESIGNATE if string.find( self.Designating[Index], "L", 1, true ) == nil then self.Designating[Index] = self.Designating[Index] .. "L" + self.LaseStart = timer.getTime() + self.LaseDuration = Duration + self:Lasing( Index, Duration, LaserCode ) end - self.LaseStart = timer.getTime() - self.LaseDuration = Duration - self:Lasing( Index, Duration, LaserCode ) end @@ -1322,7 +1330,7 @@ do -- DESIGNATE local MarkedLaserCodesText = ReportLaserCodes:Text(', ') self.CC:GetPositionable():MessageToSetGroup( "Marking " .. MarkingCount .. " x " .. MarkedTypesText .. ", code " .. MarkedLaserCodesText .. ".", 5, self.AttackSet, self.DesignateName ) - self:__Lasing( -30, Index, Duration, LaserCodeRequested ) + self:__Lasing( -self.LaseDuration, Index, Duration, LaserCodeRequested ) self:SetDesignateMenu() diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 074c15f71..499e0683e 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -69,7 +69,6 @@ -- @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! @@ -625,8 +624,7 @@ -- 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. 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. +-- are routed to the warehouse zone. -- -- 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. @@ -1557,7 +1555,6 @@ WAREHOUSE = { autosave = false, autosavepath = nil, autosavefile = nil, - saveparking = false, } --- Item of the warehouse stock table. @@ -1719,19 +1716,17 @@ WAREHOUSE.Quantity = { --- Warehouse database. Note that this is a global array to have easier exchange between warehouses. -- @type WAREHOUSE.db -- @field #number AssetID Unique ID of each asset. This is a running number, which is increased each time a new asset is added. --- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}.# --- @field #number WarehouseID Unique ID of the warehouse. Running number. +-- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}. -- @field #table Warehouses Table holding all defined @{#WAREHOUSE} objects by their unique ids. WAREHOUSE.db = { - AssetID = 0, - Assets = {}, - WarehouseID = 0, - Warehouses = {} + AssetID = 0, + Assets = {}, + Warehouses = {} } --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.6" +WAREHOUSE.version="0.6.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1740,12 +1735,12 @@ WAREHOUSE.version="0.6.6" -- 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. --- DONE: Test capturing a neutral warehouse. --- DONE: Add save/load capability of warehouse <==> percistance after mission restart. Difficult in lua! +-- TODO: 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. @@ -1792,7 +1787,7 @@ WAREHOUSE.version="0.6.6" --- The WAREHOUSE constructor. Creates a new WAREHOUSE object from a static object. Parameters like the coalition and country are taken from the static object structure. -- @param #WAREHOUSE self --- @param Wrapper.Static#STATIC warehouse The physical structure representing the warehouse. +-- @param Wrapper.Static#STATIC warehouse The physical structure of the warehouse. -- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static -- @return #WAREHOUSE self function WAREHOUSE:New(warehouse, alias) @@ -1800,11 +1795,7 @@ function WAREHOUSE:New(warehouse, alias) -- Check if just a string was given and convert to static. if type(warehouse)=="string" then - warehouse=UNIT:FindByName(warehouse) - if warehouse==nil then - env.info(string.format("No warehouse unit with name %s found trying static.", warehouse)) - warehouse=STATIC:FindByName(warehouse, true) - end + warehouse=STATIC:FindByName(warehouse, true) end -- Nil check. @@ -1827,13 +1818,7 @@ function WAREHOUSE:New(warehouse, alias) -- Set some variables. self.warehouse=warehouse - - -- Increase global warehouse counter. - WAREHOUSE.db.WarehouseID=WAREHOUSE.db.WarehouseID+1 - - -- Set unique ID for this warehouse. - self.uid=WAREHOUSE.db.WarehouseID - --self.uid=tonumber(warehouse:GetID()) + self.uid=tonumber(warehouse:GetID()) -- Closest of the same coalition but within a certain range. local _airbase=self:GetCoordinate():GetClosestAirbase(nil, self:GetCoalition()) @@ -1877,7 +1862,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", "*") -- Save the warehouse state to disk. + self:AddTransition("*", "Save", "*") -- TODO 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! @@ -2378,24 +2363,6 @@ 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. @@ -3563,13 +3530,13 @@ 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 - - -- 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 + + -- 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 -- Get the asset from the global DB. @@ -3621,7 +3588,6 @@ 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. @@ -4654,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, "AutoDefence") + self:AddRequest(self, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, WAREHOUSE.Quantity.ALL, nil, nil , 0) else text=text..string.format("No ground assets currently available.") end @@ -6330,26 +6296,25 @@ 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_dep=self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) - local termtype_des=self:_GetTerminal(asset.attribute, request.warehouse:GetAirbaseCategory()) + local termtype=self:_GetTerminal(asset.attribute) -- Get number of parking spots. - local np_departure=self.airbase:GetParkingSpotsNumber(termtype_dep) - local np_destination=request.airbase:GetParkingSpotsNumber(termtype_des) + local np_departure=self.airbase:GetParkingSpotsNumber(termtype) + local np_destination=request.airbase:GetParkingSpotsNumber(termtype) -- Debug info. - 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)) + self:T(string.format("Asset attribute = %s, terminal type = %d, spots at departure = %d, destination = %d", asset.attribute, termtype, np_departure, 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_dep, 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, 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_des, np_destination)) + self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse. Available spots = %d!", termtype, np_destination)) valid=false end @@ -6487,7 +6452,7 @@ function WAREHOUSE:_CheckRequestValid(request) self:T(text) -- Get necessary terminal type for helos or transport aircraft. - local termtype=self:_GetTerminal(request.transporttype, self:GetAirbaseCategory()) + local termtype=self:_GetTerminal(request.transporttype) -- Get number of parking spots. local np_departure=self.airbase:GetParkingSpotsNumber(termtype) @@ -6506,7 +6471,6 @@ 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. @@ -6948,13 +6912,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, _category) +function WAREHOUSE:_GetTerminal(_attribute) -- Default terminal is "large". local _terminal=AIRBASE.TerminalType.OpenBig - + + if _attribute==WAREHOUSE.Attribute.AIR_FIGHTER then -- Fighter ==> small. _terminal=AIRBASE.TerminalType.FighterAircraft @@ -6964,15 +6928,6 @@ function WAREHOUSE:_GetTerminal(_attribute, _category) 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 @@ -7047,6 +7002,20 @@ 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. @@ -7057,7 +7026,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local _asset=asset --#WAREHOUSE.Assetitem -- Get terminal type of this asset - local terminaltype=self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + local terminaltype=self:_GetTerminal(asset.attribute) -- Asset specific parking. parking[_asset.uid]={} @@ -7079,17 +7048,10 @@ 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)) - - 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. + local free=true + local problem=nil for _,obstacle in pairs(obstacles) do -- Check if aircraft overlaps with any obstacle. diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 02f83c679..61625bc4f 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -545,6 +545,10 @@ do -- ZONE_CAPTURE_COALITION -- @param #ZONE_CAPTURE_COALITION self -- @param #number Delay + -- We check if a unit within the zone is hit. + -- If it is, then we must move the zone to attack state. + self:HandleEvent( EVENTS.Hit, self.OnEventHit ) + return self end @@ -789,5 +793,20 @@ do -- ZONE_CAPTURE_COALITION end end + --- @param #ZONE_CAPTURE_COALITION self + -- @param Core.Event#EVENTDATA EventData The event data. + function ZONE_CAPTURE_COALITION:OnEventHit( EventData ) + + local UnitHit = EventData.TgtUnit + + if UnitHit then + if UnitHit:IsInZone( self.Zone ) then + self:Attack() + end + end + + end + + end diff --git a/Moose Development/Moose/Wrapper/Static.lua b/Moose Development/Moose/Wrapper/Static.lua index e624dc021..fb3d73296 100644 --- a/Moose Development/Moose/Wrapper/Static.lua +++ b/Moose Development/Moose/Wrapper/Static.lua @@ -213,3 +213,36 @@ function STATIC:ReSpawnAt( Coordinate, Heading ) SpawnStatic:ReSpawnAt( Coordinate, Heading ) end + + +--- Returns true if the unit is within a @{Zone}. +-- @param #STATIC self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the unit is within the @{Core.Zone#ZONE_BASE} +function STATIC:IsInZone( Zone ) + self:F2( { self.StaticName, Zone } ) + + if self:IsAlive() then + local IsInZone = Zone:IsVec3InZone( self:GetVec3() ) + + return IsInZone + end + return false +end + +--- Returns true if the unit is not within a @{Zone}. +-- @param #STATIC self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the unit is not within the @{Core.Zone#ZONE_BASE} +function STATIC:IsNotInZone( Zone ) + self:F2( { self.StaticName, Zone } ) + + if self:IsAlive() then + local IsInZone = not Zone:IsVec3InZone( self:GetVec3() ) + + self:T( { IsInZone } ) + return IsInZone + else + return false + end +end diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 9ef0e3f57..be195da13 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -59,12 +59,21 @@ Functional/Suppression.lua Functional/PseudoATC.lua Functional/Warehouse.lua +Ops/Airboss.lua +Ops/RecoveryTanker.lua +Ops/RescueHelo.lua + AI/AI_Balancer.lua +AI/AI_Air.lua AI/AI_A2A.lua AI/AI_A2A_Patrol.lua AI/AI_A2A_Cap.lua AI/AI_A2A_Gci.lua AI/AI_A2A_Dispatcher.lua +AI/AI_A2G.lua +AI/AI_A2G_Engage.lua +AI/AI_A2G_Patrol.lua +AI/AI_A2G_Dispatcher.lua AI/AI_Patrol.lua AI/AI_Cap.lua AI/AI_Cas.lua From c6a4e4c533b227b90ae586b098d616619753a121 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 9 Dec 2018 19:32:07 +0100 Subject: [PATCH 087/147] Warehouse v0.6.6 Again --- .../Moose/Functional/Warehouse.lua | 128 ++++++++++++------ 1 file changed, 83 insertions(+), 45 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 499e0683e..074c15f71 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! @@ -624,7 +625,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. @@ -1555,6 +1557,7 @@ WAREHOUSE = { autosave = false, autosavepath = nil, autosavefile = nil, + saveparking = false, } --- Item of the warehouse stock table. @@ -1716,17 +1719,19 @@ WAREHOUSE.Quantity = { --- Warehouse database. Note that this is a global array to have easier exchange between warehouses. -- @type WAREHOUSE.db -- @field #number AssetID Unique ID of each asset. This is a running number, which is increased each time a new asset is added. --- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}. +-- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}.# +-- @field #number WarehouseID Unique ID of the warehouse. Running number. -- @field #table Warehouses Table holding all defined @{#WAREHOUSE} objects by their unique ids. WAREHOUSE.db = { - AssetID = 0, - Assets = {}, - Warehouses = {} + AssetID = 0, + Assets = {}, + WarehouseID = 0, + Warehouses = {} } --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.4" +WAREHOUSE.version="0.6.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1735,12 +1740,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. @@ -1787,7 +1792,7 @@ WAREHOUSE.version="0.6.4" --- The WAREHOUSE constructor. Creates a new WAREHOUSE object from a static object. Parameters like the coalition and country are taken from the static object structure. -- @param #WAREHOUSE self --- @param Wrapper.Static#STATIC warehouse The physical structure of the warehouse. +-- @param Wrapper.Static#STATIC warehouse The physical structure representing the warehouse. -- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static -- @return #WAREHOUSE self function WAREHOUSE:New(warehouse, alias) @@ -1795,7 +1800,11 @@ function WAREHOUSE:New(warehouse, alias) -- Check if just a string was given and convert to static. if type(warehouse)=="string" then - warehouse=STATIC:FindByName(warehouse, true) + warehouse=UNIT:FindByName(warehouse) + if warehouse==nil then + env.info(string.format("No warehouse unit with name %s found trying static.", warehouse)) + warehouse=STATIC:FindByName(warehouse, true) + end end -- Nil check. @@ -1818,7 +1827,13 @@ function WAREHOUSE:New(warehouse, alias) -- Set some variables. self.warehouse=warehouse - self.uid=tonumber(warehouse:GetID()) + + -- Increase global warehouse counter. + WAREHOUSE.db.WarehouseID=WAREHOUSE.db.WarehouseID+1 + + -- Set unique ID for this warehouse. + self.uid=WAREHOUSE.db.WarehouseID + --self.uid=tonumber(warehouse:GetID()) -- Closest of the same coalition but within a certain range. local _airbase=self:GetCoordinate():GetClosestAirbase(nil, self:GetCoalition()) @@ -1862,7 +1877,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! @@ -2363,6 +2378,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. @@ -3530,12 +3563,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 @@ -3588,6 +3621,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. @@ -4620,7 +4654,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 @@ -6296,25 +6330,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 @@ -6452,7 +6487,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) @@ -6471,6 +6506,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. @@ -6912,13 +6948,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 @@ -6928,6 +6964,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 @@ -7002,20 +7047,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. @@ -7026,7 +7057,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]={} @@ -7048,10 +7079,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 ecbdbedd964b9a1e1153f105f1164e41126bb160 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 9 Dec 2018 19:32:39 +0100 Subject: [PATCH 088/147] Warehouse v0.6.6 Again --- .../Moose/Functional/Warehouse.lua | 128 ++++++++++++------ 1 file changed, 83 insertions(+), 45 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 499e0683e..074c15f71 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! @@ -624,7 +625,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. @@ -1555,6 +1557,7 @@ WAREHOUSE = { autosave = false, autosavepath = nil, autosavefile = nil, + saveparking = false, } --- Item of the warehouse stock table. @@ -1716,17 +1719,19 @@ WAREHOUSE.Quantity = { --- Warehouse database. Note that this is a global array to have easier exchange between warehouses. -- @type WAREHOUSE.db -- @field #number AssetID Unique ID of each asset. This is a running number, which is increased each time a new asset is added. --- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}. +-- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}.# +-- @field #number WarehouseID Unique ID of the warehouse. Running number. -- @field #table Warehouses Table holding all defined @{#WAREHOUSE} objects by their unique ids. WAREHOUSE.db = { - AssetID = 0, - Assets = {}, - Warehouses = {} + AssetID = 0, + Assets = {}, + WarehouseID = 0, + Warehouses = {} } --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.4" +WAREHOUSE.version="0.6.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1735,12 +1740,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. @@ -1787,7 +1792,7 @@ WAREHOUSE.version="0.6.4" --- The WAREHOUSE constructor. Creates a new WAREHOUSE object from a static object. Parameters like the coalition and country are taken from the static object structure. -- @param #WAREHOUSE self --- @param Wrapper.Static#STATIC warehouse The physical structure of the warehouse. +-- @param Wrapper.Static#STATIC warehouse The physical structure representing the warehouse. -- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static -- @return #WAREHOUSE self function WAREHOUSE:New(warehouse, alias) @@ -1795,7 +1800,11 @@ function WAREHOUSE:New(warehouse, alias) -- Check if just a string was given and convert to static. if type(warehouse)=="string" then - warehouse=STATIC:FindByName(warehouse, true) + warehouse=UNIT:FindByName(warehouse) + if warehouse==nil then + env.info(string.format("No warehouse unit with name %s found trying static.", warehouse)) + warehouse=STATIC:FindByName(warehouse, true) + end end -- Nil check. @@ -1818,7 +1827,13 @@ function WAREHOUSE:New(warehouse, alias) -- Set some variables. self.warehouse=warehouse - self.uid=tonumber(warehouse:GetID()) + + -- Increase global warehouse counter. + WAREHOUSE.db.WarehouseID=WAREHOUSE.db.WarehouseID+1 + + -- Set unique ID for this warehouse. + self.uid=WAREHOUSE.db.WarehouseID + --self.uid=tonumber(warehouse:GetID()) -- Closest of the same coalition but within a certain range. local _airbase=self:GetCoordinate():GetClosestAirbase(nil, self:GetCoalition()) @@ -1862,7 +1877,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! @@ -2363,6 +2378,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. @@ -3530,12 +3563,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 @@ -3588,6 +3621,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. @@ -4620,7 +4654,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 @@ -6296,25 +6330,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 @@ -6452,7 +6487,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) @@ -6471,6 +6506,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. @@ -6912,13 +6948,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 @@ -6928,6 +6964,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 @@ -7002,20 +7047,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. @@ -7026,7 +7057,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]={} @@ -7048,10 +7079,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 44f8c2a9335cb232ddf12ee8a9cc0d6202e4039e Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 9 Dec 2018 21:16:47 +0100 Subject: [PATCH 089/147] WAREHOUSE v0.6.7 SPAWNSTATIC UNIT --- .../Moose/Functional/Warehouse.lua | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 074c15f71..bca84cdfa 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -70,6 +70,7 @@ -- @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. +-- @field #boolean isunit If true, warehouse is represented by a unit instead of a static. -- @extends Core.Fsm#FSM --- Have your assets at the right place at the right time - or not! @@ -1558,6 +1559,7 @@ WAREHOUSE = { autosavepath = nil, autosavefile = nil, saveparking = false, + isunit = false, } --- Item of the warehouse stock table. @@ -1731,7 +1733,7 @@ WAREHOUSE.db = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.6" +WAREHOUSE.version="0.6.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1804,6 +1806,9 @@ function WAREHOUSE:New(warehouse, alias) if warehouse==nil then env.info(string.format("No warehouse unit with name %s found trying static.", warehouse)) warehouse=STATIC:FindByName(warehouse, true) + self.isunit=false + else + self.isunit=true end end @@ -1833,6 +1838,8 @@ function WAREHOUSE:New(warehouse, alias) -- Set unique ID for this warehouse. self.uid=WAREHOUSE.db.WarehouseID + + -- As Kalbuth found out, this would fail when using SPAWNSTATIC https://forums.eagle.ru/showthread.php?p=3703488#post3703488 --self.uid=tonumber(warehouse:GetID()) -- Closest of the same coalition but within a certain range. @@ -2908,6 +2915,7 @@ function WAREHOUSE:GetAssignment(request) return tostring(request.assignment) end +--[[ --- Get warehouse unique ID from static warehouse object. This is the ID under which you find the @{#WAREHOUSE} object in the global data base. -- @param #WAREHOUSE self -- @param #string staticname Name of the warehouse static object. @@ -2917,6 +2925,7 @@ function WAREHOUSE:GetWarehouseID(staticname) local uid=tonumber(warehouse:GetID()) return uid end +]] --- Find a warehouse in the global warehouse data base. -- @param #WAREHOUSE self @@ -6934,10 +6943,14 @@ function WAREHOUSE:_SimpleTaskFunction(Function, group) -- Task script. local DCSScript = {} --DCSScript[#DCSScript+1] = string.format('env.info(\"WAREHOUSE: Simple task function called!\") ') - DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". - DCSScript[#DCSScript+1] = string.format("local mystatic = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. - DCSScript[#DCSScript+1] = string.format('local warehouse = mystatic:GetState(mystatic, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. - DCSScript[#DCSScript+1] = string.format('%s(mygroup)', Function) -- Call the function, e.g. myfunction.(warehouse,mygroup) + DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". + if self.isunit then + DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse) -- The unit that holds the warehouse self object. + else + DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. + end + DCSScript[#DCSScript+1] = string.format('local warehouse = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. + DCSScript[#DCSScript+1] = string.format('%s(mygroup)', Function) -- Call the function, e.g. myfunction.(warehouse,mygroup) -- Create task. local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) From 43ec58fe85e535ddf8ad0d546da3da8bcc60f11a Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 9 Dec 2018 23:11:44 +0100 Subject: [PATCH 090/147] RT & RH Recovery Tanker Rescue Helo --- Moose Development/Moose/Ops/RecoveryTanker.lua | 7 +++++-- Moose Development/Moose/Ops/RescueHelo.lua | 4 +--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 4f7adaf8f..714e1cb0d 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -14,6 +14,7 @@ -- === -- -- ### Author: **funkyfranky** +-- ### Special thanks to HighwaymanEd for testing and suggesting improvements! -- -- @module Ops.RecoveryTanker -- @image MOOSE.JPG @@ -276,7 +277,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetPatternUpdateInterval() -- Moving zone: Zone 1 NM astern the carrier with radius of 1 NM. - self.zoneUpdate=ZONE_UNIT:New("Pattern Update Zone", self.carrier, UTILS.NMToMeters(1), {dx=-UTILS.NMToMeters(1), dy=0, relative_to_unit=true}) + --self.zoneUpdate=ZONE_UNIT:New("Pattern Update Zone", self.carrier, UTILS.NMToMeters(1), {dx=-UTILS.NMToMeters(1), dy=0, relative_to_unit=true}) --self.zoneUpdate:SmokeZone(SMOKECOLOR.White, 45) ----------------------- @@ -734,7 +735,8 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) -- Check if tanker flies through pattern update zone. -- TODO: Check if this can be used to update the pattern without too much disruption. - -- Could be a problem when carrier changes course since the tanker might not fligh through the zone any more. + -- Could be a problem when carrier changes course since the tanker might not fligh through the zone any more. + --[[ if self.Debug and self.zoneUpdate then local inupdatezone=self.tanker:GetUnit(1):IsInZone(self.zoneUpdate) if inupdatezone then @@ -742,6 +744,7 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) self:T(string.format("Recovery tanker is in pattern update zone! Time=%s", clock)) end end + ]] -- Check if tanker is running and not RTBing or refueling. if self:IsRunning() then diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 0a70c194c..a37c6c5be 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -9,8 +9,6 @@ -- * Automatic respawning on empty fuel for 24/7 operations. -- * Automatic rescuing of crashed or ejected units in the vicinity. -- --- Please not that his class is work in progress and in an **alpha** stage. --- -- === -- -- ### Author: **funkyfranky** @@ -162,7 +160,7 @@ RESCUEHELO = { --- Class version. -- @field #string version -RESCUEHELO.version="0.9.4w" +RESCUEHELO.version="0.9.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list From 7eb98c05eb946c165c13b45fe60768fe05def4ee Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Mon, 10 Dec 2018 16:59:00 +0100 Subject: [PATCH 091/147] AIRBOSS v0.5.0w --- Moose Development/Moose/Ops/Airboss.lua | 55 ++++++++++++++++--------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 34e699e89..b43ed3596 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -255,6 +255,8 @@ AIRBOSS.CarrierType={ -- @field #number OnSpeed Optimal on-speed AoA. -- @field #number Fast Fast AoA threshold. Smaller means faster. -- @field #number Slow Slow AoA threshold. Larger means slower. +-- @field #number FAST Really fast AoA threshold. +-- @field #number SLOW Really slow AoA threshold. --- Pattern steps. -- @type AIRBOSS.PatternStep @@ -1762,25 +1764,31 @@ function AIRBOSS:_GetAircraftAoA(playerData) if hornet then -- F/A-18C Hornet parameters + aoa.SLOW=9.8 aoa.Slow=9.3 aoa.OnSpeedMax=8.8 aoa.OnSpeed=8.1 aoa.OnSpeedMin=7.4 aoa.Fast=6.9 + aoa.FAST=6.3 elseif skyhawk then -- A-4E-C parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 + aoa.SLOW=19.0 aoa.Slow=18.5 aoa.OnSpeedMax=18.0 aoa.OnSpeed=17.5 aoa.OnSpeedMin=17.0 aoa.Fast=16.5 + aoa.FAST=16.0 elseif harrier then -- TODO: AV-8B parameters! On speed AoA? + aoa.SLOW=14.0 aoa.Slow=13.0 aoa.OnSpeedMax=12.0 aoa.OnSpeed=11.0 aoa.OnSpeedMin=10.0 aoa.Fast=9.0 + aoa.FAST=8.0 end return aoa @@ -3837,6 +3845,7 @@ function AIRBOSS:_CheckForLongDownwind(playerData) -- Sound output. self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.LONGINGROOVE) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.DEPARTANDREENTER) -- Debrief. self:_AddToDebrief(playerData, "Long in the groove - Pattern Wave Off!") @@ -5059,9 +5068,7 @@ end -- @param #number lineupError Error in degrees. function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) - -- Player group. - local player=playerData.unit:GetGroup() - + -- Advice time. local advice=0 -- Glideslope high/low calls. @@ -5162,10 +5169,10 @@ function AIRBOSS:_LSOgrade(playerData) end -- Analyse flight data and conver to LSO text. - local GXX,nXX=self:_Flightdata2Text(playerData.groove.XX) - local GIM,nIM=self:_Flightdata2Text(playerData.groove.IM) - local GIC,nIC=self:_Flightdata2Text(playerData.groove.IC) - local GAR,nAR=self:_Flightdata2Text(playerData.groove.AR) + local GXX,nXX=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.XX) --playerData.groove.XX) + local GIM,nIM=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IM) --playerData.groove.IM) + local GIC,nIC=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IC) --playerData.groove.IC) + local GAR,nAR=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.AR) --playerData.groove.AR) -- Put everything together. local G=GXX.." "..GIM.." ".." "..GIC.." "..GAR @@ -5241,10 +5248,12 @@ end --- Grade flight data. -- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string groovestep Step in the groove. -- @param #AIRBOSS.GrooveData fdata Flight data in the groove. -- @return #string LSO grade or empty string if flight data table is nil. -- @return #number Number of deviations from perfect flight path. -function AIRBOSS:_Flightdata2Text(fdata) +function AIRBOSS:_Flightdata2Text(playerData, groovestep) local function little(text) return string.format("(%s)",text) @@ -5252,6 +5261,9 @@ function AIRBOSS:_Flightdata2Text(fdata) local function underline(text) return string.format("_%s_", text) end + + -- Data. + local fdata=playerData.groove[groovestep] -- No flight data ==> return empty string. if fdata==nil then @@ -5265,40 +5277,43 @@ function AIRBOSS:_Flightdata2Text(fdata) local GSE=fdata.GSE local LUE=fdata.LUE local ROL=fdata.Roll + + -- Aircraft specific AoA values. + local acaoa=self:_GetAircraftAoA(playerData) -- Speed. local S=nil - if AOA>9.8 then + if AOA>acaoa.SLOW then S=underline("SLO") - elseif AOA>9.3 then + elseif AOA>acaoa.Slow then S="SLO" - elseif AOA>8.8 then + elseif AOA>acaoa.OnSpeedMax then S=little("SLO") - elseif AOA<6.4 then + elseif AOA1 then A=underline("H") elseif GSE>0.5 then - A=little("H") - elseif GSE>0.25 then A="H" + elseif GSE>0.25 then + A=little("H") elseif GSE<-1 then A=underline("LO") elseif GSE<-0.5 then - A=little("LO") - elseif GSE<-0.25 then A="LO" + elseif GSE<-0.25 then + A=little("LO") end - -- Line up. + -- Line up. Good [-0.5, 0.5] local D=nil if LUE>3 then D=underline("LUL") From 5ef4b593a28ef2de992ecbcfc43fd61b9c1ad349 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 11 Dec 2018 00:19:14 +0100 Subject: [PATCH 092/147] AIRBOSS v0.5.1 --- Moose Development/Moose/Ops/Airboss.lua | 409 +++++++++++++----- .../Moose/Ops/RecoveryTanker.lua | 2 +- Moose Development/Moose/Ops/RescueHelo.lua | 2 +- 3 files changed, 291 insertions(+), 122 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index b43ed3596..565566958 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -19,8 +19,8 @@ -- * Multiple carrier support due to object oriented approach. -- * Finite State Machine (FSM) implementation. -- --- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much work in progress. --- Your constructive feed back is necessary and highly appreciated. +-- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much **work in progress**. +-- Your constructive feedback is both necessary and highly appreciated. -- -- At the moment, parameters are optimized for F/A-18C Hornet as aircraft and USS John C. Stennis as carrier. -- The community A-4E mod is also supported in priciple but maybe needs further tweaking of parameters such as on speed AoA values. @@ -30,7 +30,7 @@ -- === -- -- ### Author: **funkyfranky** --- ### Special thanks to **Bankler** for his [https://forums.eagle.ru/showthread.php?t=221412](Recovery Trainer) mission and script, which gave the inspiration for this class. +-- ### Special thanks to **Bankler** for his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script! This gave the inspiration for this class. Also it uses some functionalities for determining the player positon in Case I recoveries. -- -- @module Ops.Airboss -- @image MOOSE.JPG @@ -65,7 +65,7 @@ -- @field Core.Zone#ZONE_UNIT zoneInitial Zone usually 3 NM astern of carrier where pilots start their CASE I pattern. -- @field #table players Table of players. -- @field #table menuadded Table of units where the F10 radio menu was added. --- @field #AIRBOSS.Checkpoint Upwind Upwind checkpoint. +-- @field #AIRBOSS.Checkpoint BreakEntry Break entry checkpoint. -- @field #AIRBOSS.Checkpoint BreakEarly Early break checkpoint. -- @field #AIRBOSS.Checkpoint BreakLate Late brak checkpoint. -- @field #AIRBOSS.Checkpoint Abeam Abeam checkpoint. @@ -90,6 +90,9 @@ -- @field #boolean handleai If true (default), handle AI aircraft. -- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. -- @field Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. +-- @field DCS#Vec3 Corientation Carrier orientation in space. +-- @field DCS#Vec3 Corientlast Last known carrier orientation. +-- @field Core.Point#COORDINATE Cposition Carrier position. -- @extends Core.Fsm#FSM --- The boss! @@ -162,10 +165,10 @@ AIRBOSS = { zoneInitial = nil, players = {}, menuadded = {}, - Upwind = {}, - Abeam = {}, + BreakEntry = {}, BreakEarly = {}, BreakLate = {}, + Abeam = {}, Ninety = {}, Wake = {}, Final = {}, @@ -269,16 +272,16 @@ AIRBOSS.PatternStep={ PLATFORM="Platform", ARCIN="Arc Turn In", ARCOUT="Arc Turn Out", - DIRTYUP="Level out and Dirty Up", - BULLSEYE="Follow Bullseye", + DIRTYUP="Dirty Up", + BULLSEYE="Bullseye", INITIAL="Initial", - UPWIND="Upwind", + BREAKENTRY="Break Entry", EARLYBREAK="Early Break", LATEBREAK="Late Break", ABEAM="Abeam", NINETY="Ninety", WAKE="Wake", - FINAL="On Final", + FINAL="Turn Final", GROOVE_XX="Groove X", GROOVE_RB="Groove Roger Ball", GROOVE_IM="Groove In the Middle", @@ -650,6 +653,8 @@ AIRBOSS.GroovePos={ -- @field #string grade LSO grade, i.e. _OK_, OK, (OK), --, CUT -- @field #number points Points received. -- @field #string details Detailed flight analysis. +-- @field #number wire Wire caught. +-- @field #number Tgroove Time in the groove in seconds. --- Checkpoint parameters triggering the next step in the pattern. -- @type AIRBOSS.Checkpoint @@ -717,18 +722,20 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.0" +AIRBOSS.version="0.5.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Extract (static) weather from mission for cloud covery etc. +-- TODO: First send AI to marshal and then allow them into the landing pattern ==> task function when reaching the waypoint. +-- TODO: PWO during case 2/3. Also when too close to other player. -- TODO: Option to filter AI groups for recovery. --- TODO: Check distance to players during approach. PWO if too close. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! -- TODO: Foul deck check. -- TODO: Persistence of results. +-- DONE: Extract (static) weather from mission for cloud covery etc. +-- DONE: Check distance to players during approach. -- DONE: Option to turn AI handling off. -- DONE: Add user functions. -- DONE: Update AI holding pattern wrt to moving carrier. @@ -854,8 +861,8 @@ function AIRBOSS:New(carriername, alias) return nil end - -- CASE I/II moving zone: Zone 3 NM astern and 100 m starboard of the carrier with radius of 0.5 km. - self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, 0.5*1000, {dx=-UTILS.NMToMeters(3), dy=100, relative_to_unit=true}) + -- CASE I/II moving zone: Zone 2.75 NM astern and 0.1 NM starboard of the carrier with a diameter of 1 NM. + self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, UTILS.NMToMeters(0.5), {dx=-UTILS.NMToMeters(2.75), dy=UTILS.NMToMeters(0.1), relative_to_unit=true}) -- Smoke zones. if self.Debug and false then @@ -1355,6 +1362,30 @@ function AIRBOSS:onafterStatus(From, Event, To) self:__Status(-0.5) end +--- Get aircraft nickname. +-- @param #AIRBOSS self +-- @param #string actype Aircraft type name. +-- @return #string Aircraft nickname. E.g. "Hornet" for the F/A-18C or "Tomcat" For the F-14A. +function AIRBOSS:_GetACNickname(actype) + + local nickname="unknown" + if actype==AIRBOSS.AircraftCarrier.A4EC then + nickname="Skyhawk" + elseif actype==AIRBOSS.AircraftCarrier.AV8B then + nickname="Harrier" + elseif actype==AIRBOSS.AircraftCarrier.E2D then + nickname="Hawkeye" + elseif actype==AIRBOSS.AircraftCarrier.F14A then + nickname="Tomcat" + elseif actype==AIRBOSS.AircraftCarrier.FA18C or actype==AIRBOSS.AircraftCarrier.HORNET then + nickname="Hornet" + elseif actype==AIRBOSS.AircraftCarrier.S3B or actype==AIRBOSS.AircraftCarrier.S3BTANKER then + nickname="Viking" + end + + return nickname +end + --- Check recovery times and start/stop recovery mode of aircraft. -- @param #AIRBOSS self function AIRBOSS:_CheckAIStatus() @@ -1375,27 +1406,27 @@ function AIRBOSS:_CheckAIStatus() -- Get lineup and distance to carrier. local lineup=self:_Lineup(unit, true) - local distance=unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) + local distance=UTILS.NMToMeters(unit:GetCoordinate():Get2DDistance(self:GetCoordinate())) + local alt=UTILS.MetersToFeet(unit:GetAltitude()) -- Check if parameters are right and flight is in the groove. - if lineup<2 and distance<=UTILS.NMToMeters(0.75) and not flight.ballcall then + if lineup<2 and distance<=0.75 and alt<500 and not flight.ballcall then -- Paddles: Call the ball! self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.CALLTHEBALL, false, 0) -- Pilot: "405, Hornet Ball, 3.2" - -- TODO: Hornet ==> General. -- TODO: Message to players only. -- TODO: Voice over. -- TODO: Correct unit onboard number not section lead! - local text=string.format("%s, Hornet Ball, %.1f.", flight.onboard, self:_GetFuelState(unit)/1000) + local text=string.format("%s, %s Ball, %.1f.", flight.onboard, self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) MESSAGE:New(text, 5):ToCoalition(self:GetCoalition()) --self:MessageToPlayer(playerData, text, playerData.onboard, "", 3, false, 3) - -- Paddles: Roger ball. - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 10) + -- Paddles: Roger ball after 3 seconds. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 3) - -- TODO: This does not work for flights with more than one aircraft in the group! + -- TODO: This does not work for flights with more than one aircraft in the group! But we need to set it not to flood the screen with messages for the same unit. flight.ballcall=true end @@ -1405,6 +1436,80 @@ function AIRBOSS:_CheckAIStatus() end +--- Check if player in the landing pattern is too close to another aircarft in the pattern. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData player Player data. +function AIRBOSS:_CheckPlayerPatternDistance(player) + + -- Nothing to do since we check only in the pattern. + if #self.Qpattern==0 then + return + end + + --- Function that checks if unit1 is too close to unit2. + local function _checkclose(_unit1, _unit2) + + local unit1=_unit1 --Wrapper.Unit#UNIT + local unit2=_unit2 --Wrapper.Unit#UNIT + + if (not unit1) or (not unit2) then + return false + end + + -- Check that this is not the same unit. + if unit1:GetName()==unit2:GetName() then + return false + end + + -- TODO: return false when unit2 is not in air? Could be on the carrier. + + -- Positions of units. + local c1=unit1:GetCoordinate() + local c2=unit2:GetCoordinate() + + -- Vector from unit1 to unit2 + local vec12={x=c2.x-c1.x, y=0, z=c2.z-c1.z} --DCS#Vec3 + + -- Distance between units. + local dist=UTILS.VecNorm(vec12) + + -- Orientation of unit 1 in space. + local vec1=unit1:GetOrientationX() + vec1.y=0 + + -- Get angle between the two orientation vectors. Does the player aircraft nose point into the direction of the other aircraft? (Could be behind him!) + local rhdg=math.deg(math.acos(UTILS.VecDot(vec12,vec1)/UTILS.VecNorm(vec12)/UTILS.VecNorm(vec1))) + + -- Direction in 30 degrees cone and distance < 200 meters. + -- TODO: Test parameter values. + if math.abs(rhdg)<30 and dist<200 then + return true + else + return false + end + end + + -- Loop over all other flights in pattern. + for _,_flight in pairs(self.Qpattern) do + local flight=_flight --#AIRBOSS.Flightitem + + -- Now we still need to loop over all units in the flight. + for _,_unit in pairs(flight.group:GetUnits()) do + + -- Check if player is too close to another aircraft in the pattern. + local tooclose=_checkclose(player, _unit) + + if tooclose then + local text=string.format("Player %s too close (<200 meters) to aircraft %s!", player.name, _unit:GetName()) + MESSAGE:New(text, 20, "DEBUG"):ToAllIf(self.Debug) + -- TODO: AIRBOSS call ==> Pattern wave off. + end + + end + end + +end + --- Check recovery times and start/stop recovery mode of aircraft. -- @param #AIRBOSS self function AIRBOSS:_CheckRecoveryTimes() @@ -1658,16 +1763,16 @@ function AIRBOSS:_InitStennis() self.Bullseye.LimitZmin=nil self.Bullseye.LimitZmax=nil - -- Upwind leg (break entry). - self.Upwind.name="Upwind" - self.Upwind.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. - self.Upwind.Xmax= nil - self.Upwind.Zmin=-400 -- Not more than 400 meters port of boat. Otherwise miss the zone. - self.Upwind.Zmax= 600 -- Not more than 600 m starboard of boat. Otherwise miss the zone. - self.Upwind.LimitXmin=0 -- Check and next step when at carrier and starboard of carrier. - self.Upwind.LimitXmax=nil - self.Upwind.LimitZmin=-100 - self.Upwind.LimitZmax=nil + -- Break entry. + self.BreakEntry.name="BreakEntry" + self.BreakEntry.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. + self.BreakEntry.Xmax= nil + self.BreakEntry.Zmin=-400 -- Not more than 400 meters port of boat. Otherwise miss the zone. + self.BreakEntry.Zmax= 600 -- Not more than 600 m starboard of boat. Otherwise miss the zone. + self.BreakEntry.LimitXmin=0 -- Check and next step when at carrier and starboard of carrier. + self.BreakEntry.LimitXmax=nil + self.BreakEntry.LimitZmin=-100 + self.BreakEntry.LimitZmax=nil -- Early break. self.BreakEarly.name="Early Break" @@ -1862,7 +1967,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) speed=UTILS.KnotsToMps(250) end - elseif step==AIRBOSS.PatternStep.UPWIND then + elseif step==AIRBOSS.PatternStep.BREAKENTRY then if hornet then alt=UTILS.FeetToMeters(800) @@ -2236,7 +2341,7 @@ function AIRBOSS:_MarshalAI(flight, nstack) end -- Landing waypoint. - wp[#wp+1]=Carrier:SetAltitude(250):WaypointAirLanding(Speed, self.airbase, nil, "Landing") + --wp[#wp+1]=Carrier:SetAltitude(250):WaypointAirLanding(Speed, self.airbase, nil, "Landing") -- Reinit waypoints. group:WayPointInitialize(wp) @@ -2245,6 +2350,32 @@ function AIRBOSS:_MarshalAI(flight, nstack) group:Route(wp, 0) end +--- Tell AI to land on the carrier. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Flightitem flight Flight group. +function AIRBOSS:_LandAI(flight) + + -- Aircraft speed when flying the pattern. + local Speed=UTILS.KnotsToMps(272) + + local Carrier=self:GetCoordinate() + + -- Waypoints array. + local wp={} + + wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil ,Speed, {}, "Current position") + + -- Landing waypoint. + wp[#wp+1]=self:GetCoordinate():SetAltitude(250):WaypointAirLanding(Speed, self.airbase, nil, "Landing") + + -- Reinit waypoints. + flight.group:WayPointInitialize(wp) + + -- Route group. + flight.group:Route(wp, 0) + +end + --- Get marshal altitude and position. -- @param #AIRBOSS self -- @param #number stack Assigned stack number. Counting starts at one, i.e. stack=1 is the first stack. @@ -2353,6 +2484,7 @@ function AIRBOSS:_CheckCollapseMarshalStack(flight) if flight.ai then -- Collapse stack and send AI to pattern. self:_CollapseMarshalStack(flight) + self:_LandAI(flight) end -- Inform all flights. @@ -2723,6 +2855,7 @@ function AIRBOSS:_InitPlayer(playerData) playerData.Tlso=timer.getTime() playerData.Tgroove=nil playerData.wire=nil + playerData.ballcall=false -- Set us up on final if group name contains "Groove". But only for the first pass. if playerData.group:GetName():match("Groove") and playerData.passes==0 then @@ -2921,7 +3054,7 @@ function AIRBOSS:_CheckPatternUpdate() -- Current orientation of carrier. local vNew=self.carrier:GetOrientationX() - -- Reference orientation of carrier after the last update + -- Reference orientation of carrier after the last update. local vOld=self.Corientation -- Last orientation from 30 seconds ago. @@ -3014,6 +3147,10 @@ function AIRBOSS:_CheckPlayerStatus() -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). if unit:IsInZone(self.zoneCCA) then + + -- Check if player is too close to another aircraft in the pattern. + -- TODO: Find a better place to call this! + --self:_CheckPlayerPatternDistance(playerData) if playerData.step==AIRBOSS.PatternStep.UNDEFINED then @@ -3070,20 +3207,20 @@ function AIRBOSS:_CheckPlayerStatus() -- CASE I/II: Player is at the initial position entering the landing pattern. self:_Initial(playerData) - elseif playerData.step==AIRBOSS.PatternStep.UPWIND then + elseif playerData.step==AIRBOSS.PatternStep.BREAKENTRY then - -- CASE I/II: Upwind leg aka break entry. - self:_Upwind(playerData) + -- CASE I/II: Break entry. + self:_BreakEntry(playerData) elseif playerData.step==AIRBOSS.PatternStep.EARLYBREAK then -- CASE I/II: Early break. - self:_Break(playerData, "early") + self:_Break(playerData, AIRBOSS.PatternStep.EARLYBREAK) elseif playerData.step==AIRBOSS.PatternStep.LATEBREAK then -- CASE I/II: Late break. - self:_Break(playerData, "late") + self:_Break(playerData, AIRBOSS.PatternStep.LATEBREAK) elseif playerData.step==AIRBOSS.PatternStep.ABEAM then @@ -3266,9 +3403,14 @@ function AIRBOSS:OnEventLand(EventData) coord:SmokeGreen() end - -- Get wire + -- Get wire. local wire=self:_GetWire(dist) + -- No wire ==> Bolter, Bolter radio call. + if wire>4 then + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.BOLTER) + end + -- Get time in the groove. local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData playerData.Tgroove=timer.getTime()-gdataX0.TGroove @@ -3498,15 +3640,18 @@ function AIRBOSS:_Initial(playerData) -- Inform player. local hint=string.format("Initial") - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - hint=hint.."\nAim for 800 feet and 350 kts at the break entry." + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + local alt,aoa,dist,speed=self:_GetAircraftParameters(playerData, AIRBOSS.PatternStep.BREAKENTRY) + hint=hint..string.format("\nOptimal setup at the break entry is %d feet and %d kts.", UTILS.MetersToFeet(alt), UTILS.MpsToKnots(speed)) end - -- Send message. - self:MessageToPlayer(playerData, hint, "MARSHAL") + -- Send message for normal and easy difficulty. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + self:MessageToPlayer(playerData, hint, "MARSHAL") + end - -- Next step: upwind. - playerData.step=AIRBOSS.PatternStep.UPWIND + -- Next step: Break entry. + playerData.step=AIRBOSS.PatternStep.BREAKENTRY end end @@ -3741,22 +3886,22 @@ function AIRBOSS:_Bullseye(playerData) end ---- Upwind leg or break entry for case I/II recoveries. +--- Break entry for case I/II recoveries. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Upwind(playerData) +function AIRBOSS:_BreakEntry(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi=self:_GetDistances(playerData.unit) -- Abort condition check. - if self:_CheckAbort(X, Z, self.Upwind) then - self:_AbortPattern(playerData, X, Z, self.Upwind, true) + if self:_CheckAbort(X, Z, self.BreakEntry) then + self:_AbortPattern(playerData, X, Z, self.BreakEntry, true) return end -- Check if we are in front of the boat (diffX > 0). - if self:_CheckLimits(X, Z, self.Upwind) then + if self:_CheckLimits(X, Z, self.BreakEntry) then -- Get optimal altitude, distance and speed. local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) @@ -3791,7 +3936,7 @@ function AIRBOSS:_Break(playerData, part) -- Early or late break. local breakpoint = self.BreakEarly - if part=="late" then + if part==AIRBOSS.PatternStep.LATEBREAK then breakpoint = self.BreakLate end @@ -3820,7 +3965,7 @@ function AIRBOSS:_Break(playerData, part) self:_AddToDebrief(playerData, debrief) -- Next step: Late Break or Abeam. - if part=="early" then + if part==AIRBOSS.PatternStep.EARLYBREAK then playerData.step=AIRBOSS.PatternStep.LATEBREAK else playerData.step=AIRBOSS.PatternStep.ABEAM @@ -5773,6 +5918,8 @@ function AIRBOSS:_Debrief(playerData) mygrade.grade=grade mygrade.points=points mygrade.details=analysis + mygrade.wire=playerData.wire + mygrade.Tgroove=playerData.Tgroove -- Add grade to table. table.insert(playerData.grades, mygrade) @@ -5799,34 +5946,52 @@ function AIRBOSS:_Debrief(playerData) if playerData.unit:IsAlive() then -- TODO: handle case where player landed even though he was waved off! + + if playerData.unit:InAir()==true then - -- Heading and distance tip. - local heading, distance - - if playerData.case==1 or playerData.case==2 then - - -- Get heading and distance to initial zone ~3 NM astern. - heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) - distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) - - elseif playerData.case==3 then - - -- Get heading and distance to bullseye zone ~3 NM astern. - local zone=self:_GetZoneBullseye(playerData.case) + -- Heading and distance tip. + local heading, distance - heading=playerData.unit:GetCoordinate():HeadingTo(zone:GetCoordinate()) - distance=playerData.unit:GetCoordinate():Get2DDistance(zone:GetCoordinate()) + if playerData.case==1 or playerData.case==2 then + + -- Get heading and distance to initial zone ~3 NM astern. + heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) + distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) + + elseif playerData.case==3 then + + -- Get heading and distance to bullseye zone ~3 NM astern. + local zone=self:_GetZoneBullseye(playerData.case) + + heading=playerData.unit:GetCoordinate():HeadingTo(zone:GetCoordinate()) + distance=playerData.unit:GetCoordinate():Get2DDistance(zone:GetCoordinate()) + + end + + -- Re-enter message. + local text=string.format("fly heading %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) + self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 5) + + + -- Commencing again. + playerData.step=AIRBOSS.PatternStep.COMMENCING + playerData.warning=nil + + else + + if playerData.waveoff then + + -- Airboss talkto! + local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") + self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 2) + + -- Next step undefined. Player landed. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + playerData.warning=nil + + end end - - -- Re-enter message. - local text=string.format("fly heading %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) - self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 5) - - - -- Commencing again. - playerData.step=AIRBOSS.PatternStep.COMMENCING - playerData.warning=nil else -- Unit does not seem to be alive! @@ -6320,25 +6485,29 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration text=string.format("%s, %s", receiver, message) end self:I(self.lid..text) - - -- Send onboard number so that player is alerted about the text message. - -- DONE: This will fail with message to all since for each player the message will be played! - if receiver==playerData.onboard and not soundoff then - if sender then - if sender=="LSO" then - self:_Number2Sound(self.LSORadio, receiver, delay) - elseif sender=="MARSHAL" then - self:_Number2Sound(self.MarshalRadio, receiver, delay) - end - end - end if delay and delay>0 then - SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) + -- Delayed call. + SCHEDULER:New(self, self.MessageToPlayer, {playerData, message, sender, receiver, duration, clear, 0, soundoff}, delay) else + + -- Send onboard number so that player is alerted about the text message. + -- DONE: This will fail with message to all since for each player the message will be played! + if receiver==playerData.onboard and not soundoff then + if sender then + if sender=="LSO" then + self:_Number2Sound(self.LSORadio, receiver, delay) + elseif sender=="MARSHAL" then + self:_Number2Sound(self.MarshalRadio, receiver, delay) + end + end + end + + -- Text message to player client. if playerData.client then MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) end + end end @@ -6496,47 +6665,47 @@ function AIRBOSS:_AddF10Commands(_unitName) -- F10/Airboss//F1 Help -------------------------------- local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) - -- F10/Airboss//Help/Skill Level + -- F10/Airboss//F1 Help/F1 Skill Level local _skillPath=missionCommands.addSubMenuForGroup(gid, "Skill Level", _helpPath) - -- F10/Airboss//Help/Skill Level/ - missionCommands.addCommandForGroup(gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) - missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) - missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) - -- F10/Airboss//Help/Mark Zones + -- F10/Airboss//F1 Help/F1 Skill Level/ + missionCommands.addCommandForGroup(gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) -- F1 + missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) -- F2 + missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) -- F3 + -- F10/Airboss//F1 Help/F2 Mark Zones local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) - -- F10/Airboss//Help/Mark Zones/ - missionCommands.addCommandForGroup(gid, "Smoke My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) - missionCommands.addCommandForGroup(gid, "Flare My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) - missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) - missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) - -- F10/Airboss//Help/ - missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) - missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) - missionCommands.addCommandForGroup(gid, "Attitude Monitor ON/OFF", _helpPath, self._AttitudeMonitor, self, playername) - missionCommands.addCommandForGroup(gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) + -- F10/Airboss//F1 Help/F3 Mark Zones/ + missionCommands.addCommandForGroup(gid, "Smoke My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F1 + missionCommands.addCommandForGroup(gid, "Flare My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F2 + missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F3 + missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F4 + -- F10/Airboss//F1 Help/ + missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F4 + missionCommands.addCommandForGroup(gid, "Attitude Monitor ON/OFF", _helpPath, self._AttitudeMonitor, self, playername) -- F5 + missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) -- F6 + missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) -- F7 + missionCommands.addCommandForGroup(gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) -- F8 ------------------------------------- -- F10/Airboss//F2 Kneeboard -- ------------------------------------- local _kneeboardPath=missionCommands.addSubMenuForGroup(gid, "Kneeboard", _rootPath) - -- F10/Airboss//Kneeboard/Results + -- F10/Airboss//F2 Kneeboard/F1 Results local _resultsPath=missionCommands.addSubMenuForGroup(gid, "Results", _kneeboardPath) - -- F10/Airboss//Kneeboard/Results/ - missionCommands.addCommandForGroup(gid, "Greenie Board", _resultsPath, self._DisplayScoreBoard, self, _unitName) - missionCommands.addCommandForGroup(gid, "My LSO Grades", _resultsPath, self._DisplayPlayerGrades, self, _unitName) - missionCommands.addCommandForGroup(gid, "Last Debrief", _resultsPath, self._DisplayDebriefing, self, _unitName) - -- F10/Airboss//F2 Kneeboard/F1 Results/ + missionCommands.addCommandForGroup(gid, "Greenie Board", _resultsPath, self._DisplayScoreBoard, self, _unitName) -- F1 + missionCommands.addCommandForGroup(gid, "My LSO Grades", _resultsPath, self._DisplayPlayerGrades, self, _unitName) -- F2 + missionCommands.addCommandForGroup(gid, "Last Debrief", _resultsPath, self._DisplayDebriefing, self, _unitName) -- F3 + -- F10/Airboss// -- ---------------------------- - missionCommands.addCommandForGroup(gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) - missionCommands.addCommandForGroup(gid, "Request Commence", _rootPath, self._RequestCommence, self, _unitName) - missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) + missionCommands.addCommandForGroup(gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "Request Commence", _rootPath, self._RequestCommence, self, _unitName) -- F4 + missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) -- F5 end else self:T(self.lid.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 714e1cb0d..8d3ec8236 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -1,4 +1,4 @@ ---- **Functional** - (R2.5) - Carrier recovery tanker. +--- **Ops** - (R2.5) - Carrier recovery tanker. -- -- Tanker aircraft flying a racetrack pattern overhead an aircraft carrier. -- diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index a37c6c5be..15e58b986 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -1,4 +1,4 @@ ---- **Functional** - (R2.5) - Rescue helo. +--- **Ops** - (R2.5) - Rescue helo. -- -- Recue helicopter for carrier operations. -- From 143e729a5e85fa7cb2a30012525afa1d3292f5d9 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 11 Dec 2018 16:02:45 +0100 Subject: [PATCH 093/147] AIRBOSS v0.5.1w --- Moose Development/Moose/Ops/Airboss.lua | 158 +++++++++++++++++++++++- 1 file changed, 153 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 565566958..3320356ff 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -30,7 +30,9 @@ -- === -- -- ### Author: **funkyfranky** --- ### Special thanks to **Bankler** for his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script! This gave the inspiration for this class. Also it uses some functionalities for determining the player positon in Case I recoveries. +-- ### Special thanks to +-- **Bankler** for his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script! +-- This gave the inspiration for this class. Also this uses some functionalities for determining the player positon in Case I recoveries. -- -- @module Ops.Airboss -- @image MOOSE.JPG @@ -134,6 +136,58 @@ -- ## CASE II -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case2.png) +-- +-- # Scripting +-- +-- Writing a basic script is easy and can be done in two lines. +-- +-- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") +-- airbossStennis:Start() +-- +-- The first line creates and AIRBOSS object via the @{#AIRBOSS.New}(*carriername*, *alias*) constructor. The first parameter *carriername* is name of the carrier unit as +-- defined in the mission editor. The second parameter *alias* is optional. This name will, e.g., be used for the F10 radio menu entry. If not given, the alias is identical +-- to the carriername of the first parameter. +-- +-- This simple script initializes a lot of parameters with default values: +-- +-- * TACAN channel is set to 74X, see @{#AIRBOSS.SetTACAN} +-- * ICSL channel is set to 1, see @{#AIRBOSS.SetICLS} +-- * LSO radio is set to 264 MHz FM, see @{#AIRBOSS.SetLSORadio} +-- * Marshal radio is set to 305 MHz FM, see @{#AIRBOSS.SetMarshalRadio} +-- * Default recovery case is set to 1, see @{#AIRBOSS.SetRecoveryCase} +-- +-- ## Recovery Windows +-- +-- Recovery of aircraft is only allowed during defined time slots. You can define these slots via the @{#AIRBOSS.AddRecoveryWindow}(*start*, *stop*, *case*, *holdingoffset*) function. +-- The parameters are: +-- +-- * *start*: The start time as a string. For example "8:00" for a window opening at 8 am. Or "13:30+1" for half past one on the next day. Default (nil) is ASAP. +-- * *stop*: Time when the window closes as a string. Same format as *start*. Default is 90 minutes after start time. +-- * *case*: The recovery case during that window (1, 2 or 3). Default 1. +-- * *holdingoffset*: Holding offset angle in degrees. Only for Case II or III recoveries. Default 0 deg. Common +-15 deg or +-30 deg. +-- +-- If recovery is closed, AI flights will be send to marshal stacks and orbit there until the next window opens. +-- Players can request marshal via the F10 menu and will also be given a marshal stack. Currently, human players can request commence via the F10 radio regarless of +-- whether a window is open or not and will be alowed to enter the pattern (if not already full). This will probably change in the future. +-- +-- At the moment there is no autmatic recovery case set depending on weather or daytime. So it is the AIRBOSS (you) who needs to make that descision. +-- It is probably a good idea to synchronize the timing with the waypoints of the carrier. For example, setting up the waypoints such that the carrier +-- already has turning into the wind, when a recovery window opens. +-- +-- The code for setting up multiple recovery windows could look like this +-- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") +-- airbossStennis:AddRecoveryWindow("8:30", "9:30", 1) +-- airbossStennis:AddRecoveryWindow("12:00", "13:15", 2, 15) +-- airbossStennis:AddRecoveryWindow("23:30", "00:30+1", 3, -30) +-- airbossStennis:Start() +-- +-- This will open a Case I recovery window from 8:30 to 9:30. Then a Case II recovery from 12:00 to 13:15, where the holing offset is +15 degrees wrt BRC. +-- Finally, a Case III window opens 23:30 on the day the mission starts and closes 0:30 on the following day. The holding offset is -30 degrees wrt FB. +-- +-- Note that incoming flights will be assigned a holding pattern for the next opening window case if no window is open at the moment. So in the above example, +-- all flights incoming after 13:15 will be assigned to a Case III marshal stack. Therefore, you should make sure that no flights are incoming long before the +-- next window opens or adjust the recovery planning accordingly. +-- -- -- @field #AIRBOSS AIRBOSS = { @@ -722,7 +776,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.1" +AIRBOSS.version="0.5.1w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1066,7 +1120,7 @@ end -- @param #number case Recovery case for that time slot. Number between one and three. -- @param #number holdingoffset Only for CASE II/III: Angle in degrees the holding pattern is offset. -- @return #AIRBOSS self -function AIRBOSS:AddRecoveryTime(starttime, stoptime, case, holdingoffset) +function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset) -- Absolute mission time in seconds. local Tnow=timer.getAbsTime() @@ -1724,11 +1778,21 @@ function AIRBOSS:_InitStennis() self.carrierparam.rwyangle = -9 self.carrierparam.sterndist =-150 self.carrierparam.deckheight = 22 + + --[[ self.carrierparam.wire1 =-104 self.carrierparam.wire2 = -92 self.carrierparam.wire3 = -80 self.carrierparam.wire4 = -68 self.carrierparam.wireoffset = 30 + ]] + + self.carrierparam.wire1 = 0 + self.carrierparam.wire2 = 12 + self.carrierparam.wire3 = 24 + self.carrierparam.wire4 = 36 + self.carrierparam.wireoffset = 30 + -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight. self.Platform.name="Platform 5k" @@ -1764,7 +1828,7 @@ function AIRBOSS:_InitStennis() self.Bullseye.LimitZmax=nil -- Break entry. - self.BreakEntry.name="BreakEntry" + self.BreakEntry.name="Break Entry" self.BreakEntry.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. self.BreakEntry.Xmax= nil self.BreakEntry.Zmin=-400 -- Not more than 400 meters port of boat. Otherwise miss the zone. @@ -2350,6 +2414,25 @@ function AIRBOSS:_MarshalAI(flight, nstack) group:Route(wp, 0) end +--- Task function. +-- @param #AIRBOSS self +function AIRBOSS:_InitPatternTaskFunction() + + -- Name of the warehouse (static) object. + local carriername=self.carrier:GetName() + + -- Task script. + local DCSScript = {} + DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername) -- The carrier unit that holds the self object. + DCSScript[#DCSScript+1] = string.format('local myairboss = mycarrier:GetState(mycarrier, \"AIRBOSS\") ') -- Get the AIRBOSS self object. + DCSScript[#DCSScript+1] = string.format('myairboss:PatternUpdate()') -- Call the function, e.g. mytanker.(self) + + -- Create task. + local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + + return DCSTask +end + --- Tell AI to land on the carrier. -- @param #AIRBOSS self -- @param #AIRBOSS.Flightitem flight Flight group. @@ -4441,11 +4524,74 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) return waveoff end +--- Get wire from landing position. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE Ccoord Carrier position. +-- @param Core.Point#COORDINATE Landing position. +-- @param #number dx Correction. +function AIRBOSS:_GetWire(Ccoord, Lcoord, dx) + + local hdg=self.carrier:GetHeading() + + -- Stern coordinate (sterndist<0) + local Scoord=Ccoord:Translate(self.carrierparam.sterndist, hdg) + + -- Distance to landing coord + local Ldist=Lcoord:Get2DDistance(Scoord) + + -- Little offset for the exact wire positions. + dx=dx or self.carrierparam.wireoffset + + -- Corrected distance. + local d=Ldist+dx + + -- Which wire was caught? X>0 since calculated as distance! + local wire + if d wire=%d.", Ldist, dx, d, wire)) + + return wire +end + --- Get wire from landing position. -- @param #AIRBOSS self -- @param #number d Distance in meters wrt carrier position where player landed. -- @param #number dx Correction. -function AIRBOSS:_GetWire(d, dx) +function AIRBOSS:_GetWire2(d, dx) -- Little offset for the exact wire positions. dx=dx or self.carrierparam.wireoffset @@ -4498,9 +4644,11 @@ function AIRBOSS:_Trapped(playerData) self:_AddToDebrief(playerData, hint, "Groove: IW") else + --Still in air ==> Boltered! MESSAGE:New("Player boltered in trapped", 5, "DEBUG") playerData.boltered=true + end -- Next step: debriefing. From d098930351c2aaef2a9f05bc4e7735ab47792633 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 11 Dec 2018 19:20:11 +0100 Subject: [PATCH 094/147] -- New SEAD, BAI and CAS files. -- Correct handling of task assignments. -- Added Weapon stuff in DCS.lua. -- Dispatcher optimizations. --- Moose Development/Moose/AI/AI_A2A.lua | 2 +- Moose Development/Moose/AI/AI_A2G_BAI.lua | 172 ++++++++++++++ Moose Development/Moose/AI/AI_A2G_CAS.lua | 173 ++++++++++++++ .../Moose/AI/AI_A2G_Dispatcher.lua | 36 ++- Moose Development/Moose/AI/AI_A2G_Engage.lua | 78 +------ Moose Development/Moose/AI/AI_A2G_Patrol.lua | 3 +- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 218 ++++++++++++++++++ Moose Development/Moose/AI/AI_Air.lua | 6 +- Moose Development/Moose/DCS.lua | 122 ++++++++++ .../Moose/Wrapper/Controllable.lua | 55 ++++- Moose Setup/Moose.files | 3 + 11 files changed, 775 insertions(+), 93 deletions(-) create mode 100644 Moose Development/Moose/AI/AI_A2G_BAI.lua create mode 100644 Moose Development/Moose/AI/AI_A2G_CAS.lua create mode 100644 Moose Development/Moose/AI/AI_A2G_SEAD.lua diff --git a/Moose Development/Moose/AI/AI_A2A.lua b/Moose Development/Moose/AI/AI_A2A.lua index c96fa4e43..14c137dc2 100644 --- a/Moose Development/Moose/AI/AI_A2A.lua +++ b/Moose Development/Moose/AI/AI_A2A.lua @@ -489,7 +489,7 @@ function AI_A2A:onafterStatus() not self:Is( "Fuel" ) and not self:Is( "Damaged" ) and not self:Is( "Home" ) then - if self.IdleCount >= 2 then + if self.IdleCount >= 3 then if Damage ~= InitialLife then self:Damaged() else diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua new file mode 100644 index 000000000..fb72297d9 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -0,0 +1,172 @@ +--- **AI** -- Models the process of air to ground BAI engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_BAI +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_BAI +-- @extends AI.AI_A2A_Engage#AI_A2A_Engage + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- === +-- +-- @field #AI_A2G_BAI +AI_A2G_BAI = { + ClassName = "AI_A2G_BAI", +} + + + +--- Creates a new AI_A2G_BAI object +-- @param #AI_A2G_BAI self +-- @param Wrapper.Group#GROUP AIGroup +-- @return #AI_A2G_BAI +function AI_A2G_BAI:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) ) -- #AI_A2G_BAI + + return self +end + +--- onafter event handler for Start event. +-- @param #AI_A2G_BAI self +-- @param Wrapper.Group#GROUP AIGroup The AI group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_BAI:onafterStart( AIGroup, From, Event, To ) + + self:GetParent( self ).onafterStart( self, AIGroup, From, Event, To ) + AIGroup:HandleEvent( EVENTS.Takeoff, nil, self ) + +end + + + +--- @param #AI_A2G_BAI self +-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + + local AttackCount = self.AttackSetUnit:Count() + + if AttackCount > 0 then + + if DefenderGroup:IsAlive() then + + -- Determine the distance to the target. + -- If it is less than 10km, then attack without a route. + -- Otherwise perform a route attack. + + local DefenderCoord = DefenderGroup:GetCoordinate() + local TargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + + if TargetDistance >= 50000 then + + local EngageRoute = {} + + local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromWP + + local ToCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + self:SetTargetDistance( ToCoord ) -- For RTB status check + + local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) + + --- Create a route point of type air. + local ToWP = ToCoord:Translate( 50000, FromEngageAngle ):WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = FromEngageAngle, ToTargetSpeed = ToTargetSpeed } ) + self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToWP + + local AttackTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "Engage Unit evaluation:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) + local HasRadar = AttackUnit:HasSEAD() + if HasRadar then + self:T( { "Eliminating Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) + end + end + end + + if #AttackTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + end + + DefenderGroup:Route( EngageRoute, 0 ) + + else + local AttackTasks = {} + --local AttackUnit = self.AttackSetUnit:GetRandom() -- Wrapper.Unit#UNIT + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + local HasRadar = AttackUnit:HasSEAD() + if HasRadar then + self:T( { "Eliminating Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) + end + end + end + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + local DefenderTask = DefenderGroup:TaskCombo( AttackTasks ) + DefenderGroup:SetTask( DefenderTask, 0 ) + end + end + else + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + end +end diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua new file mode 100644 index 000000000..85a1e1d8a --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -0,0 +1,173 @@ +--- **AI** -- Models the process of air to ground engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_CAS +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_CAS +-- @extends AI.AI_A2G_Engage#AI_A2G_Engage + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- === +-- +-- @field #AI_A2G_CAS +AI_A2G_CAS = { + ClassName = "AI_A2G_CAS", +} + + + +--- Creates a new AI_A2G_CAS object +-- @param #AI_A2G_CAS self +-- @param Wrapper.Group#GROUP AIGroup +-- @return #AI_A2G_CAS +function AI_A2G_CAS:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) ) -- #AI_A2G_CAS + + return self +end + +--- onafter event handler for Start event. +-- @param #AI_A2G_CAS self +-- @param Wrapper.Group#GROUP AIGroup The AI group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_CAS:onafterStart( AIGroup, From, Event, To ) + + self:GetParent( self ).onafterStart( self, AIGroup, From, Event, To ) + AIGroup:HandleEvent( EVENTS.Takeoff, nil, self ) + +end + + + +--- @param #AI_A2G_CAS self +-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + + local AttackCount = self.AttackSetUnit:Count() + + if AttackCount > 0 then + + if DefenderGroup:IsAlive() then + + -- Determine the distance to the target. + -- If it is less than 10km, then attack without a route. + -- Otherwise perform a route attack. + + local DefenderCoord = DefenderGroup:GetCoordinate() + local TargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + + if TargetDistance >= 50000 then + + local EngageRoute = {} + + local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromWP + + local ToCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + self:SetTargetDistance( ToCoord ) -- For RTB status check + + local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) + + --- Create a route point of type air. + local ToWP = ToCoord:Translate( 50000, FromEngageAngle ):WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = FromEngageAngle, ToTargetSpeed = ToTargetSpeed } ) + self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToWP + + local AttackTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "Engage Unit evaluation:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) + local HasRadar = AttackUnit:HasSEAD() + if HasRadar then + self:T( { "Eliminating Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) + end + end + end + + if #AttackTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + end + + DefenderGroup:Route( EngageRoute, 0 ) + + else + local AttackTasks = {} + --local AttackUnit = self.AttackSetUnit:GetRandom() -- Wrapper.Unit#UNIT + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + local HasRadar = AttackUnit:HasSEAD() + if HasRadar then + self:T( { "Eliminating Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) + end + end + end + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + local DefenderTask = DefenderGroup:TaskCombo( AttackTasks ) + DefenderGroup:SetTask( DefenderTask, 0 ) + end + end + else + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + end +end + diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index fde91d028..76cf4be62 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -810,6 +810,7 @@ do -- AI_A2G_DISPATCHER if Squadron then self:F( { SquadronName = Squadron.Name } ) local LandingMethod = self:GetSquadronLanding( Squadron.Name ) + if LandingMethod == AI_A2G_DISPATCHER.Landing.AtRunway then local DefenderSize = Defender:GetSize() if DefenderSize == 1 then @@ -1395,6 +1396,8 @@ do -- AI_A2G_DISPATCHER local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.Uncontrolled = true + self:SetSquadronTakeoffFromParkingCold( SquadronName ) + self:SetSquadronLandingAtEngineShutdown( SquadronName ) for SpawnTemplate, DefenderSpawn in pairs( self.DefenderSpawns ) do DefenderSpawn:InitUnControlled() @@ -2856,13 +2859,16 @@ do -- AI_A2G_DISPATCHER if DefenderUnitIndex == 1 then DefenderPatrolTemplate = UTILS.DeepCopy( DefenderTemplate ) self.DefenderPatrolIndex = self.DefenderPatrolIndex + 1 - DefenderPatrolTemplate.name = SquadronName .. "#" .. self.DefenderPatrolIndex .. "#" .. GroupName + --DefenderPatrolTemplate.name = SquadronName .. "#" .. self.DefenderPatrolIndex .. "#" .. GroupName + DefenderPatrolTemplate.name = GroupName DefenderName = DefenderPatrolTemplate.name else -- Add the unit in the template to the DefenderPatrolTemplate. local DefenderUnitTemplate = DefenderTemplate.units[1] DefenderPatrolTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate end + DefenderPatrolTemplate.units[DefenderUnitIndex].name = string.format( DefenderPatrolTemplate.name .. '-%02d', DefenderUnitIndex ) + DefenderPatrolTemplate.units[DefenderUnitIndex].unitId = nil DefenderUnitIndex = DefenderUnitIndex + 1 DefenderSquadron.Resources[TemplateID][GroupName] = nil if DefenderUnitIndex > DefenderGrouping then @@ -2880,8 +2886,8 @@ do -- AI_A2G_DISPATCHER DefenderPatrolTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type DefenderPatrolTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action local Defender = _DATABASE:Spawn( DefenderPatrolTemplate ) - self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + Defender:Activate() return Defender, DefenderGrouping end else @@ -2993,6 +2999,8 @@ do -- AI_A2G_DISPATCHER self:F( { From, Event, To, AttackerDetection.Index, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing, DefenderFriendlies = DefenderFriendlies } ) + AttackerDetection.Type = DefenseTaskType -- This is set to report the task type in the status panel. + local AttackerSet = AttackerDetection.Set local AttackerUnit = AttackerSet:GetFirst() @@ -3103,7 +3111,9 @@ do -- AI_A2G_DISPATCHER DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead - local Fsm = AI_A2G_ENGAGE:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed ) + local AI_A2G_ENGAGE = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + + local Fsm = AI_A2G_ENGAGE[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed ) -- AI.AI_A2G_ENGAGE Fsm:SetDispatcher( self ) Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) @@ -3121,6 +3131,8 @@ do -- AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) + self:F( { DefenderTarget = DefenderTarget } ) + if DefenderTarget then Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit end @@ -3141,10 +3153,10 @@ do -- AI_A2G_DISPATCHER local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - if Defender:IsAboveRunway() then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end + --if Defender:IsAboveRunway() then + --Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + --Defender:Destroy() + --end end --- @param #AI_A2G_DISPATCHER self @@ -3298,8 +3310,8 @@ do -- AI_A2G_DISPATCHER for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do local DefenderGroup = DefenderGroup -- Wrapper.Group#GROUP + local DefenderTaskFsm = self:GetDefenderTaskFsm( DefenderGroup ) if not DefenderGroup:IsAlive() then - local DefenderTaskFsm = self:GetDefenderTaskFsm( DefenderGroup ) self:F( { Defender = DefenderGroup:GetName(), DefenderState = DefenderTaskFsm:GetState() } ) if not DefenderTaskFsm:Is( "Started" ) then self:ClearDefenderTask( DefenderGroup ) @@ -3312,10 +3324,10 @@ do -- AI_A2G_DISPATCHER self:ClearDefenderTaskTarget( DefenderGroup ) else if DefenderTask.Target.Set then - local AttackerCount = DefenderTask.Target.Set:Count() - if AttackerCount == 0 then + local TargetCount = DefenderTask.Target.Set:Count() + if TargetCount == 0 then self:F( { "All Targets destroyed in Target, removing:", DefenderTask.Target.Index } ) - self:ClearDefenderTaskTarget( DefenderGroup ) + self:ClearDefenderTask( DefenderGroup ) end end end @@ -3410,7 +3422,7 @@ do -- AI_A2G_DISPATCHER if self.TacticalDisplay then -- Show tactical situation - Report:Add( string.format( "\n - Target %s ( %s ): ( #%d ) %s" , DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) ) + Report:Add( string.format( "\n - %s %s ( %s ): ( #%d ) %s" , DetectedItem.Type or " --- ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) ) for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do local Defender = Defender -- Wrapper.Group#GROUP if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then diff --git a/Moose Development/Moose/AI/AI_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua index 2cc6845b1..a0513933f 100644 --- a/Moose Development/Moose/AI/AI_A2G_Engage.lua +++ b/Moose Development/Moose/AI/AI_A2G_Engage.lua @@ -14,7 +14,7 @@ --- @type AI_A2G_ENGAGE --- @extends AI.AI_A2A#AI_A2A +-- @extends AI.AI_A2G#AI_A2G --- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. @@ -266,7 +266,8 @@ end -- @param #string To The To State string. function AI_A2G_ENGAGE:onafterStart( AIGroup, From, Event, To ) - self:GetParent( self ).onafterStart( self, AIGroup, From, Event, To ) + self:GetParent( self, AI_A2G_ENGAGE ).onafterStart( self, AIGroup, From, Event, To ) + AIGroup:HandleEvent( EVENTS.Takeoff, nil, self ) end @@ -327,81 +328,14 @@ end --- @param #AI_A2G_ENGAGE self --- @param Wrapper.Group#GROUP AIGroup The GroupGroup managed by the FSM. +-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. -function AI_A2G_ENGAGE:onafterEngage( AIGroup, From, Event, To, AttackSetUnit ) +function AI_A2G_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) - self:F( { AIGroup, From, Event, To, AttackSetUnit} ) - - self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) - local FirstAttackUnit = self.AttackSetUnit:GetFirst() - - if FirstAttackUnit and FirstAttackUnit:IsAlive() then - - if AIGroup:IsAlive() then - - local EngageRoute = {} - - local CurrentCoord = AIGroup:GetCoordinate() - - --- Calculate the target route point. - - local CurrentCoord = AIGroup:GetCoordinate() - - local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - self:SetTargetDistance( ToTargetCoord ) -- For RTB status check - - local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - local ToEngageAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = CurrentCoord:Translate( 15000, ToEngageAngle ):WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = ToEngageAngle, ToTargetSpeed = ToTargetSpeed } ) - self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - - local AttackTasks = {} - - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:T( { "Eliminating Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) - AttackTasks[#AttackTasks+1] = AIGroup:TaskAttackUnit( AttackUnit ) - end - end - - if #AttackTasks == 0 then - self:E("No targets found -> Going RTB") - self:Return() - self:__RTB( 0.5 ) - else - AIGroup:OptionROEOpenFire() - AIGroup:OptionROTEvadeFire() - - AttackTasks[#AttackTasks+1] = AIGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) - EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( AttackTasks ) - end - - AIGroup:Route( EngageRoute, 0.5 ) - - end - else - self:E("No targets found -> Going RTB") - self:Return() - self:__RTB( 0.5 ) - end end --- @param #AI_A2G_ENGAGE self diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua index bf4a97dba..9a667b5cc 100644 --- a/Moose Development/Moose/AI/AI_A2G_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2G_Patrol.lua @@ -112,7 +112,7 @@ AI_A2G_PATROL = { function AI_A2G_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, PatrolAltType ) -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2A_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_PATROL + local self = BASE:Inherit( self, AI_A2G:New( AIPatrol ) ) -- #AI_A2G_PATROL self.Accomplished = false self.Engaging = false @@ -290,7 +290,6 @@ end -- @param #string To The To State string. function AI_A2G_PATROL:onafterStart( AIPatrol, From, Event, To ) - self:GetParent( self ).onafterStart( self, AIPatrol, From, Event, To ) AIPatrol:HandleEvent( EVENTS.Takeoff, nil, self ) end diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua new file mode 100644 index 000000000..c0db300d4 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -0,0 +1,218 @@ +--- **AI** -- Models the process of air to ground SEAD engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_SEAD +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_SEAD +-- @extends AI.AI_A2G_Engage#AI_A2G_Engage + + +--- Implements the core functions to SEAD intruders. Use the Engage trigger to intercept intruders. +-- +-- ![Process](..\Presentations\AI_GCI\Dia3.JPG) +-- +-- The AI_A2G_SEAD is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_SEAD process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_GCI\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_GCI\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_GCI\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_GCI\Dia13.JPG) +-- +-- ## 1. AI_A2G_SEAD constructor +-- +-- * @{#AI_A2G_SEAD.New}(): Creates a new AI_A2G_SEAD object. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_GCI\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_GCI#AI_A2G_SEAD.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_A2G_SEAD.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2G_SEAD +AI_A2G_SEAD = { + ClassName = "AI_A2G_SEAD", +} + + + +--- Creates a new AI_A2G_SEAD object +-- @param #AI_A2G_SEAD self +-- @param Wrapper.Group#GROUP AIGroup +-- @return #AI_A2G_SEAD +function AI_A2G_SEAD:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) ) -- #AI_A2G_SEAD + + return self +end + + + +--- @param #AI_A2G_SEAD self +-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + + local AttackCount = self.AttackSetUnit:Count() + + if AttackCount > 0 then + + if DefenderGroup:IsAlive() then + + -- Determine the distance to the target. + -- If it is less than 50km, then attack without a route. + -- Otherwise perform a route attack. + + local DefenderCoord = DefenderGroup:GetCoordinate() + local TargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + +-- if TargetDistance >= 50000 then + + local EngageRoute = {} + + local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromWP + + local ToCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + self:SetTargetDistance( ToCoord ) -- For RTB status check + + local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) + + --- Create a route point of type air, 50km from the center of the attack point. + local ToWP = ToCoord:Translate( 50000, FromEngageAngle ):WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = FromEngageAngle, ToTargetSpeed = ToTargetSpeed } ) + self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToWP + + local AttackTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "Engage Unit evaluation:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) + local HasRadar = AttackUnit:HasSEAD() + if HasRadar then + self:T( { "Eliminating Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) + end + end + end + + if #AttackTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTVertical() + DefenderGroup:OptionKeepWeaponsOnThreat() + --DefenderGroup:OptionRTBAmmo( Weapon.flag.AnyASM ) + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + end + + DefenderGroup:Route( EngageRoute, 2 ) + +-- else +-- local AttackTasks = {} +-- --local AttackUnit = self.AttackSetUnit:GetRandom() -- Wrapper.Unit#UNIT +-- for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do +-- if AttackUnit:IsAlive() and AttackUnit:IsGround() then +-- local HasRadar = AttackUnit:HasSEAD() +-- if HasRadar then +-- self:T( { "Eliminating Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) +-- AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) +-- AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) +-- end +-- end +-- end +-- local DefenderTask = DefenderGroup:TaskCombo( AttackTasks ) +-- +-- DefenderGroup:OptionROEOpenFire() +-- DefenderGroup:OptionROTVertical() +-- DefenderGroup:OptionKeepWeaponsOnThreat() +-- DefenderGroup:OptionRTBAmmo( Weapon.flag.AnyASM ) +-- +-- DefenderGroup:SetTask( DefenderTask, 0 ) +-- end + end + else + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + end +end + diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 80a58bf7f..8425159cf 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -475,7 +475,7 @@ function AI_AIR:onafterStatus() RTB = true self:SetStatusOff() end - + -- Check if planes went RTB and are out of control. -- We only check if planes are out of control, when they are in duty. if self.Controllable:HasTask() == false then @@ -484,7 +484,7 @@ function AI_AIR:onafterStatus() not self:Is( "Fuel" ) and not self:Is( "Damaged" ) and not self:Is( "Home" ) then - if self.IdleCount >= 2 then + if self.IdleCount >= 10 then if Damage ~= InitialLife then self:Damaged() else @@ -547,7 +547,7 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) self:E( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) self:ClearTargetDistance() - AIGroup:ClearTasks() + --AIGroup:ClearTasks() local EngageRoute = {} diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index d4af6e979..002ea566d 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -430,6 +430,8 @@ do -- Types end -- + + do -- Object --- [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object) @@ -527,6 +529,126 @@ do -- CoalitionObject end -- CoalitionObject +do -- Weapon + + --- [DCS Class Weapon](https://wiki.hoggitworld.com/view/DCS_Class_Weapon) + -- @type Weapon + -- @extends #CoalitionObject + -- @field #Weapon.flag flag enum stores weapon flags. Some of them are combination of another flags. + -- @field #Weapon.Category Category enum that stores weapon categories. + -- @field #Weapon.GuidanceType GuidanceType enum that stores guidance methods. Available only for guided weapon (Weapon.Category.MISSILE and some Weapon.Category.BOMB). + -- @field #Weapon.MissileCategory MissileCategory enum that stores missile category. Available only for missiles (Weapon.Category.MISSILE). + -- @field #Weapon.WarheadType WarheadType enum that stores warhead types. + -- @field #Weapon.Desc Desc The descriptor of a weapon. + + --- enum stores weapon flags. Some of them are combination of another flags. + -- @type Weapon.flag + -- @field LGB + -- @field TvGB + -- @field SNSGB + -- @field HEBomb + -- @field Penetrator + -- @field NapalmBomb + -- @field FAEBomb + -- @field ClusterBomb + -- @field Dispencer + -- @field CandleBomb + -- @field ParachuteBomb + -- @field GuidedBomb = LGB + TvGB + SNSGB + -- @field AnyUnguidedBomb = HEBomb + Penetrator + NapalmBomb + FAEBomb + ClusterBomb + Dispencer + CandleBomb + ParachuteBomb + -- @field AnyBomb = GuidedBomb + AnyUnguidedBomb + -- @field LightRocket + -- @field MarkerRocket + -- @field CandleRocket + -- @field HeavyRocket + -- @field AnyRocket = LightRocket + HeavyRocket + MarkerRocket + CandleRocket + -- @field AntiRadarMissile + -- @field AntiShipMissile + -- @field AntiTankMissile + -- @field FireAndForgetASM + -- @field LaserASM + -- @field TeleASM + -- @field CruiseMissile + -- @field GuidedASM = LaserASM + TeleASM + -- @field TacticASM = GuidedASM + FireAndForgetASM + -- @field AnyASM = AntiRadarMissile + AntiShipMissile + AntiTankMissile + FireAndForgetASM + GuidedASM + CruiseMissile + -- @field SRAAM + -- @field MRAAM + -- @field LRAAM + -- @field IR_AAM + -- @field SAR_AAM + -- @field AR_AAM + -- @field AnyAAM = IR_AAM + SAR_AAM + AR_AAM + SRAAM + MRAAM + LRAAM + -- @field AnyMissile = AnyASM + AnyAAM + -- @field AnyAutonomousMissile = IR_AAM + AntiRadarMissile + AntiShipMissile + FireAndForgetASM + CruiseMissile + -- @field GUN_POD + -- @field BuiltInCannon + -- @field Cannons = GUN_POD + BuiltInCannon + -- @field AnyAGWeapon = BuiltInCannon + GUN_POD + AnyBomb + AnyRocket + AnyASM + -- @field AnyAAWeapon = BuiltInCannon + GUN_POD + AnyAAM + -- @field UnguidedWeapon = Cannons + BuiltInCannon + GUN_POD + AnyUnguidedBomb + AnyRocket + -- @field GuidedWeapon = GuidedBomb + AnyASM + AnyAAM + -- @field AnyWeapon = AnyBomb + AnyRocket + AnyMissile + Cannons + -- @field MarkerWeapon = MarkerRocket + CandleRocket + CandleBomb + -- @field ArmWeapon = AnyWeapon - MarkerWeapon + + --- Weapon.Category enum that stores weapon categories. + -- @type Weapon.Category + -- @field SHELL + -- @field MISSILE + -- @field ROCKET + -- @field BOMB + + + --- Weapon.GuidanceType enum that stores guidance methods. Available only for guided weapon (Weapon.Category.MISSILE and some Weapon.Category.BOMB). + -- @type Weapon.GuidanceType + -- @field INS + -- @field IR + -- @field RADAR_ACTIVE + -- @field RADAR_SEMI_ACTIVE + -- @field RADAR_PASSIVE + -- @field TV + -- @field LASER + -- @field TELE + + + --- Weapon.MissileCategory enum that stores missile category. Available only for missiles (Weapon.Category.MISSILE). + -- @type Weapon.MissileCategory + -- @field AAM + -- @field SAM + -- @field BM + -- @field ANTI_SHIP + -- @field CRUISE + -- @field OTHER + + --- Weapon.WarheadType enum that stores warhead types. + -- @type Weapon.WarheadType + -- @field AP + -- @field HE + -- @field SHAPED_EXPLOSIVE + + --- Returns the unit that launched the weapon. + -- @function [parent=#Weapon] getLauncher + -- @param #Weapon self + -- @return #Unit + + --- returns target of the guided weapon. Unguided weapons and guided weapon that is targeted at the point on the ground will return nil. + -- @function [parent=#Weapon] getTarget + -- @param #Weapon self + -- @return #Object + + --- returns weapon descriptor. Descriptor type depends on weapon category. + -- @function [parent=#Weapon] getDesc + -- @param #Weapon self + -- @return #Weapon.Desc + + + + Weapon = {} --#Weapon + +end -- Weapon + + do -- Airbase --- [DCS Class Airbase](https://wiki.hoggitworld.com/view/DCS_Class_Airbase) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 1fb50baa5..f917d2535 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -168,6 +168,11 @@ -- * @{#CONTROLLABLE.OptionAlarmStateGreen} -- * @{#CONTROLLABLE.OptionAlarmStateRed} -- +-- ## 5.4) Jettison weapons: +-- +-- * @{#CONTROLLABLE.OptionAllowJettisonWeaponsOnThreat} +-- * @{#CONTROLLABLE.OptionKeepWeaponsOnThreat} +-- -- @field #CONTROLLABLE CONTROLLABLE = { ClassName = "CONTROLLABLE", @@ -302,7 +307,7 @@ end -- @param #CONTROLLABLE self -- @return #CONTROLLABLE function CONTROLLABLE:ClearTasks() - self:F2() + self:E( "ClearTasks" ) local DCSControllable = self:GetDCSObject() @@ -366,13 +371,15 @@ end -- @param #number WaitTime Time in seconds, before the task is set. -- @return Wrapper.Controllable#CONTROLLABLE self function CONTROLLABLE:SetTask( DCSTask, WaitTime ) - self:F2( { DCSTask = DCSTask } ) + self:E( { "SetTask", WaitTime, DCSTask = DCSTask } ) local DCSControllable = self:GetDCSObject() if DCSControllable then local DCSControllableName = self:GetName() + + self:E( "Controllable Name = " .. DCSControllableName ) -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. -- Therefore we schedule the functions to set the mission and options for the Controllable. @@ -1659,6 +1666,7 @@ function CONTROLLABLE:TaskFunction( FunctionString, ... ) local DCSScript = {} DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:Find( ... ) " + DCSScript[#DCSScript+1] = "env.info( 'TaskFunction: ' .. ( MissionControllable and MissionControllable:GetName() ) or 'No Group' )" if arg and arg.n > 0 then local ArgumentKey = '_' .. tostring( arg ):match("table: (.*)") @@ -2922,7 +2930,7 @@ function CONTROLLABLE:OptionRTBAmmo( WeaponsFlag ) local Controller = self:_GetController() if self:IsAir() then - Controller:setOption( AI.Option.GROUND.id.RTB_ON_OUT_OF_AMMO, WeaponsFlag ) + Controller:setOption( AI.Option.Air.id.RTB_ON_OUT_OF_AMMO, WeaponsFlag ) end return self @@ -2932,6 +2940,47 @@ function CONTROLLABLE:OptionRTBAmmo( WeaponsFlag ) end +--- Allow to Jettison of weapons upon threat. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionAllowJettisonWeaponsOnThreat() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.PROHIBIT_JETT, false ) + end + + return self + end + + return nil +end + + +--- Keep weapons upon threat. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionKeepWeaponsOnThreat() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.PROHIBIT_JETT, true ) + end + + return self + end + + return nil +end + diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 231861fe0..11d2da7db 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -68,6 +68,9 @@ AI/AI_A2A_Gci.lua AI/AI_A2A_Dispatcher.lua AI/AI_A2G.lua AI/AI_A2G_Engage.lua +AI/AI_A2G_BAI.lua +AI/AI_A2G_CAS.lua +AI/AI_A2G_SEAD.lua AI/AI_A2G_Patrol.lua AI/AI_A2G_Dispatcher.lua AI/AI_Patrol.lua From d607b960217f72c444e2f93ae666274875f862b9 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 12 Dec 2018 00:50:42 +0100 Subject: [PATCH 095/147] AIRBOSS v0.5.2 --- Moose Development/Moose/Ops/Airboss.lua | 198 ++++++++++++++++-- .../Moose/Wrapper/Positionable.lua | 8 + 2 files changed, 189 insertions(+), 17 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 3320356ff..3d084eb50 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -47,6 +47,8 @@ -- @field #AIRBOSS.CarrierParameters carrierparam Carrier specifc parameters. -- @field #string alias Alias of the carrier. -- @field Wrapper.Airbase#AIRBASE airbase Carrier airbase object. +-- @field #table waypoints Waypoint coordinates of carrier. +-- @field #number currentwp Current waypoint, i.e. the one that has been passed last. -- @field Core.Radio#BEACON beacon Carrier beacon for TACAN and ICLS. -- @field #boolean TACANon Automatic TACAN is activated. -- @field #number TACANchannel TACAN channel. @@ -192,13 +194,15 @@ -- @field #AIRBOSS AIRBOSS = { ClassName = "AIRBOSS", - Debug = false, + Debug = true, lid = nil, carrier = nil, carriertype = nil, carrierparam = {}, alias = nil, airbase = nil, + waypoints = {}, + currentwp = nil, beacon = nil, TACANon = nil, TACANchannel = nil, @@ -776,7 +780,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.1w" +AIRBOSS.version="0.5.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1358,13 +1362,15 @@ function AIRBOSS:onafterStart(From, Event, To) -- TODO: id's to self to be able to stop the scheduler. local RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQLSO, "LSO"}, 1, 0.01) local RQMid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQMarshal, "MARSHAL"}, 1, 0.01) - - + -- Initial carrier position and orientation. self.Cposition=self:GetCoordinate() self.Corientation=self.carrier:GetOrientationX() self.Corientlast=self.Corientation self.Tpupdate=timer.getTime() + + -- Init patrol route of carrier. + self:_PatrolRoute() -- Start status check in 1 second. self:__Status(1) @@ -1387,7 +1393,8 @@ function AIRBOSS:onafterStatus(From, Event, To) local clock=UTILS.SecondsToClock(timer.getAbsTime()) -- Debug info. - local text=string.format("Time %s - Status %s (case %d)", clock, self:GetState(), self.case) + local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s", + clock, self:GetState(), self.case, self.carrier:GetVelocityKNOTS(), self:GetHeading(), self.currentwp, UTILS.SecondsToClock(self:_GetETAatNextWP())) self:I(self.lid..text) -- Check recovery times and start/stop recovery mode if necessary. @@ -1770,6 +1777,161 @@ end -- Parameter initialization ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Function called when group is passing a waypoint. +--@param Wrapper.Group#GROUP Group that passed the waypoint +--@param #AIRBOSS airboss Airboss object. +--@param #number i Waypoint number that has been reached. +--@param #number final Final waypoint number. +function AIRBOSS._PassingWaypoint(group, airboss, i, final) + + -- Debug message. + local text=string.format("Group %s passing waypoint %d of %d.", group:GetName(), i, final) + + local pos=group:GetCoordinate() + pos:SmokeRed() + local MarkerID=pos:MarkToAll(string.format("Reached Waypoint %d of group %s", i, group:GetName())) + + MESSAGE:New(text,10):ToAll() + env.info(text) + + -- Set current waypoint. + airboss.currentwp=i +end + + +--- Patrol carrier +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:_PatrolRoute() + + -- Get carrier group. + local CarrierGroup=self.carrier:GetGroup() + + -- Waypoints of group. + local Waypoints = CarrierGroup:GetTemplateRoutePoints() + + -- NOTE: This is only necessary, if the first waypoint would already be far way, i.e. when the script is started with a large delay. + -- Calculate the new Route. + --local wp0=CarrierGroup:GetCoordinate():WaypointGround(5.5*3.6) + + -- Insert current coordinate as first waypoint + --table.insert(Waypoints, 1, wp0) + + for n=1,#Waypoints do + + -- Passing waypoint taskfunction + local TaskPassingWP=CarrierGroup:TaskFunction("AIRBOSS._PassingWaypoint", self, n, #Waypoints) + + -- Call task function when carrier arrives at waypoint. + CarrierGroup:SetTaskWaypoint(Waypoints[n], TaskPassingWP) + end + + -- TODO: set task to call this routine again when carrier reaches final waypoint if user chooses to. + -- SetPatrolAdInfinitum user function + + -- Set waypoint table. + local i=1 + for _,point in ipairs(Waypoints) do + + -- Coordinate of the waypoint + local coord=COORDINATE:New(point.x, point.alt, point.y) + + -- Set velocity of the coordinate. + coord:SetVelocity(point.speed) + + -- Add to table. + table.insert(self.waypoints, coord) + + -- Debug info. + if self.Debug then + coord:MarkToAll(string.format("Carrier Waypoint %d, Speed=%.1f knots", i, UTILS.MpsToKnots(point.speed))) + end + + -- Increase counter. + i=i+1 + end + + -- Current waypoint is 1. + self.currentwp=1 + + -- Route carrier group. + CarrierGroup:Route(Waypoints) +end + +--- Estimated the carrier position at some point in the future given the current waypoints and speeds. +-- @param #AIRBOSS self +-- @return DCS#time ETA abs. time in seconds. +function AIRBOSS:_GetETAatNextWP() + + -- Current waypoint + local cwp=self.currentwp + + -- Current abs. time. + local tnow=timer.getAbsTime() + + -- Current position. + local p=self:GetCoordinate() + + -- Current velocity [m/s]. + local v=self.carrier:GetVelocityMPS() + + -- Distance to next waypoint. + local s=0 + if #self.waypoints>cwp then + s=p:Get2DDistance(self.waypoints[cwp+1]) + end + + -- v=s/t <==> t=s/v + local t=s/v + + -- ETA + local eta=t+tnow + + return eta +end + + +--- Estimated the carrier position at some point in the future given the current waypoints and speeds. +-- @param #AIRBOSS self +-- @param #number time Absolute mission time at which the carrier position is requested. +-- @return Core.Point#COORDINATE Coordinate of the carrier at the given time. +function AIRBOSS:_GetCarrierFuture(time) + + local nwp=self.currentwp + + local waypoints={} + local lastwp=nil --Core.Point#COORDINATE + for i=1,#self.waypoints do + + if i>nwp then + table.insert(waypoints, self.waypoints[i]) + elseif i==nwp then + lastwp=self.waypoints[i] + end + + end + + -- Current abs. time. + local tnow=timer.getAbsTime() + + local p=self:GetCoordinate() + local v=self.carrier:GetVelocityMPS() + + local s=p:Get2DDistance(self.waypoints[nwp+1]) + + -- v=s/t <==> t=s/v + local t=s/v + + local eta=UTILS.SecondsToClock(t+tnow) + + + for _,_wp in ipairs(waypoints) do + local wp=_wp --Core.Point#COORDINATE + + end + +end + --- Init parameters for USS Stennis carrier. -- @param #AIRBOSS self function AIRBOSS:_InitStennis() @@ -1831,11 +1993,11 @@ function AIRBOSS:_InitStennis() self.BreakEntry.name="Break Entry" self.BreakEntry.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. self.BreakEntry.Xmax= nil - self.BreakEntry.Zmin=-400 -- Not more than 400 meters port of boat. Otherwise miss the zone. - self.BreakEntry.Zmax= 600 -- Not more than 600 m starboard of boat. Otherwise miss the zone. + self.BreakEntry.Zmin=-400 -- Not more than 400 m port of boat. Otherwise miss the zone. + self.BreakEntry.Zmax=1000 -- Not more than 1000 m starboard of boat. Otherwise miss the zone. self.BreakEntry.LimitXmin=0 -- Check and next step when at carrier and starboard of carrier. self.BreakEntry.LimitXmax=nil - self.BreakEntry.LimitZmin=-100 + self.BreakEntry.LimitZmin=nil self.BreakEntry.LimitZmax=nil -- Early break. @@ -3234,6 +3396,8 @@ function AIRBOSS:_CheckPlayerStatus() -- Check if player is too close to another aircraft in the pattern. -- TODO: Find a better place to call this! --self:_CheckPlayerPatternDistance(playerData) + local Tnow=timer.getTime() + env.info(string.format("T=%s step=%s", Tnow, playerData.step)) if playerData.step==AIRBOSS.PatternStep.UNDEFINED then @@ -3409,7 +3573,7 @@ function AIRBOSS:OnEventBirth(EventData) self.players[_playername]=self:_NewPlayer(_unitName) -- Debug. - if self.Debug then + if self.Debug and false then self:_Number2Sound(self.LSORadio, "0123456789", 10) self:_Number2Sound(self.MarshalRadio, "0123456789", 20) end @@ -3476,10 +3640,10 @@ function AIRBOSS:OnEventLand(EventData) local hdg=self.carrier:GetHeading()+self.carrierparam.rwyangle -- Debug marks of wires. - local w1=self:GetCoordinate():Translate(self.carrierparam.wire1, hdg):MarkToAll("Wire 1") - local w2=self:GetCoordinate():Translate(self.carrierparam.wire2, hdg):MarkToAll("Wire 2") - local w3=self:GetCoordinate():Translate(self.carrierparam.wire3, hdg):MarkToAll("Wire 3") - local w4=self:GetCoordinate():Translate(self.carrierparam.wire4, hdg):MarkToAll("Wire 4") + local w1=self:GetCoordinate():Translate(self.carrierparam.wire1, hdg):MarkToAll("Wire 1a") + local w2=self:GetCoordinate():Translate(self.carrierparam.wire2, hdg):MarkToAll("Wire 2a") + local w3=self:GetCoordinate():Translate(self.carrierparam.wire3, hdg):MarkToAll("Wire 3a") + local w4=self:GetCoordinate():Translate(self.carrierparam.wire4, hdg):MarkToAll("Wire 4a") -- Debug mark of player landing coord. local lp=coord:MarkToAll("Landing coord.") @@ -3487,7 +3651,7 @@ function AIRBOSS:OnEventLand(EventData) end -- Get wire. - local wire=self:_GetWire(dist) + local wire=self:_GetWire(self:GetCoordinate(), coord) -- No wire ==> Bolter, Bolter radio call. if wire>4 then @@ -3529,7 +3693,7 @@ function AIRBOSS:OnEventLand(EventData) local dist=coord:Get2DDistance(self:GetCoordinate()) -- Get wire - local wire=self:_GetWire(dist, 0) + local wire=self:_GetWire(self:GetCoordinate(), coord, 0) -- Aircraft type. local _type=EventData.IniUnit:GetTypeName() @@ -3993,7 +4157,7 @@ function AIRBOSS:_BreakEntry(playerData) local hintAlt=self:_AltitudeCheck(playerData, alt) -- Get speed hint. - local hintSpeed=self:_AltitudeCheck(playerData, speed) + local hintSpeed=self:_SpeedCheck(playerData,speed) -- Message to player. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then @@ -4527,7 +4691,7 @@ end --- Get wire from landing position. -- @param #AIRBOSS self -- @param Core.Point#COORDINATE Ccoord Carrier position. --- @param Core.Point#COORDINATE Landing position. +-- @param Core.Point#COORDINATE Lcoord Landing position. -- @param #number dx Correction. function AIRBOSS:_GetWire(Ccoord, Lcoord, dx) diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 137d4ab4c..ee8c9b011 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -656,6 +656,14 @@ function POSITIONABLE:GetVelocityMPS() return 0 end +--- Returns the POSITIONABLE velocity in knots. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number The velocity in knots. +function POSITIONABLE:GetVelocityKNOTS() + self:F2( self.PositionableName ) + return UTILS.MpsToKnots(self:GetVelocityMPS()) +end + --- Returns the Angle of Attack of a positionable. -- @param Wrapper.Positionable#POSITIONABLE self -- @return #number Angle of attack in degrees. From 810cebfe0a1b204268dadbd2d6ba18a57d9977c0 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 12 Dec 2018 07:31:08 +0100 Subject: [PATCH 096/147] Optimizations --- Moose Development/Moose/AI/AI_A2G_BAI.lua | 146 +++++++----------- Moose Development/Moose/AI/AI_A2G_CAS.lua | 145 +++++++---------- .../Moose/AI/AI_A2G_Dispatcher.lua | 5 +- Moose Development/Moose/AI/AI_Air.lua | 13 +- Moose Development/Moose/Wrapper/Group.lua | 2 +- Moose Development/Moose/Wrapper/Unit.lua | 4 +- 6 files changed, 126 insertions(+), 189 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index fb72297d9..5c91e4027 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -40,20 +40,6 @@ function AI_A2G_BAI:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) return self end ---- onafter event handler for Start event. --- @param #AI_A2G_BAI self --- @param Wrapper.Group#GROUP AIGroup The AI group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_BAI:onafterStart( AIGroup, From, Event, To ) - - self:GetParent( self ).onafterStart( self, AIGroup, From, Event, To ) - AIGroup:HandleEvent( EVENTS.Takeoff, nil, self ) - -end - - --- @param #AI_A2G_BAI self -- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. @@ -83,86 +69,64 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) - if TargetDistance >= 50000 then - - local EngageRoute = {} - - local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - - --- Calculate the target route point. - - local FromWP = DefenderCoord:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = FromWP - - local ToCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - self:SetTargetDistance( ToCoord ) -- For RTB status check - - local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) - - --- Create a route point of type air. - local ToWP = ToCoord:Translate( 50000, FromEngageAngle ):WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = FromEngageAngle, ToTargetSpeed = ToTargetSpeed } ) - self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToWP - - local AttackTasks = {} - - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:T( { "Engage Unit evaluation:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) - local HasRadar = AttackUnit:HasSEAD() - if HasRadar then - self:T( { "Eliminating Unit:", AttackUnit:GetName() } ) - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) - end - end - end - - if #AttackTasks == 0 then - self:E( DefenderGroupName .. ": No targets found -> Going RTB") - self:Return() - self:__RTB( 0.5 ) - else - DefenderGroup:OptionROEOpenFire() - DefenderGroup:OptionROTEvadeFire() - - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) - EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) - end - - DefenderGroup:Route( EngageRoute, 0 ) + local EngageRoute = {} + + local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - else - local AttackTasks = {} - --local AttackUnit = self.AttackSetUnit:GetRandom() -- Wrapper.Unit#UNIT - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - local HasRadar = AttackUnit:HasSEAD() - if HasRadar then - self:T( { "Eliminating Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) - end - end + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromWP + + local ToCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + self:SetTargetDistance( ToCoord ) -- For RTB status check + + local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) + + --- Create a route point of type air. + local ToWP = ToCoord:Translate( 10000, FromEngageAngle ):WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = FromEngageAngle, ToTargetSpeed = ToTargetSpeed } ) + self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToWP + + local AttackTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "Engage Unit evaluation:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) + self:T( { "Eliminating Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) end + end + + if #AttackTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) - local DefenderTask = DefenderGroup:TaskCombo( AttackTasks ) - DefenderGroup:SetTask( DefenderTask, 0 ) - end + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + end + + DefenderGroup:Route( EngageRoute, 0.5 ) end else self:E( DefenderGroupName .. ": No targets found -> Going RTB") diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 85a1e1d8a..4177b38ed 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -40,20 +40,6 @@ function AI_A2G_CAS:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) return self end ---- onafter event handler for Start event. --- @param #AI_A2G_CAS self --- @param Wrapper.Group#GROUP AIGroup The AI group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_CAS:onafterStart( AIGroup, From, Event, To ) - - self:GetParent( self ).onafterStart( self, AIGroup, From, Event, To ) - AIGroup:HandleEvent( EVENTS.Takeoff, nil, self ) - -end - - --- @param #AI_A2G_CAS self -- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. @@ -83,86 +69,63 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) - if TargetDistance >= 50000 then - - local EngageRoute = {} - - local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - - --- Calculate the target route point. - - local FromWP = DefenderCoord:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = FromWP - - local ToCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - self:SetTargetDistance( ToCoord ) -- For RTB status check - - local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) - - --- Create a route point of type air. - local ToWP = ToCoord:Translate( 50000, FromEngageAngle ):WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = FromEngageAngle, ToTargetSpeed = ToTargetSpeed } ) - self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToWP - - local AttackTasks = {} - - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:T( { "Engage Unit evaluation:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) - local HasRadar = AttackUnit:HasSEAD() - if HasRadar then - self:T( { "Eliminating Unit:", AttackUnit:GetName() } ) - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) - end - end - end - - if #AttackTasks == 0 then - self:E( DefenderGroupName .. ": No targets found -> Going RTB") - self:Return() - self:__RTB( 0.5 ) - else - DefenderGroup:OptionROEOpenFire() - DefenderGroup:OptionROTEvadeFire() - - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) - EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) - end - - DefenderGroup:Route( EngageRoute, 0 ) + local EngageRoute = {} + + local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - else - local AttackTasks = {} - --local AttackUnit = self.AttackSetUnit:GetRandom() -- Wrapper.Unit#UNIT - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - local HasRadar = AttackUnit:HasSEAD() - if HasRadar then - self:T( { "Eliminating Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) - end - end + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromWP + + local ToCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + self:SetTargetDistance( ToCoord ) -- For RTB status check + + local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) + + --- Create a route point of type air. + local ToWP = ToCoord:Translate( 10000, FromEngageAngle ):WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = FromEngageAngle, ToTargetSpeed = ToTargetSpeed } ) + self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToWP + + local AttackTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "Eliminating Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) end + end + + if #AttackTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) - local DefenderTask = DefenderGroup:TaskCombo( AttackTasks ) - DefenderGroup:SetTask( DefenderTask, 0 ) - end + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + end + + DefenderGroup:Route( EngageRoute, 0.5 ) end else self:E( DefenderGroupName .. ": No targets found -> Going RTB") diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 76cf4be62..13c6767d5 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3175,7 +3175,7 @@ do -- AI_A2G_DISPATCHER if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) Defender:Destroy() - self:ParkDefender( Squadron, Defender ) + Dispatcher:ParkDefender( Squadron, Defender ) end end end -- if DefenderGCI then @@ -3311,6 +3311,9 @@ do -- AI_A2G_DISPATCHER for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do local DefenderGroup = DefenderGroup -- Wrapper.Group#GROUP local DefenderTaskFsm = self:GetDefenderTaskFsm( DefenderGroup ) + if DefenderTaskFsm:Is( "LostControl" ) then + self:ClearDefenderTask( DefenderGroup ) + end if not DefenderGroup:IsAlive() then self:F( { Defender = DefenderGroup:GetName(), DefenderState = DefenderTaskFsm:GetState() } ) if not DefenderTaskFsm:Is( "Started" ) then diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 8425159cf..85d125384 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -425,7 +425,6 @@ function AI_AIR:onafterStatus() if not self:Is( "Holding" ) and not self:Is( "Returning" ) then local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) - self:F({DistanceFromHomeBase=DistanceFromHomeBase}) if DistanceFromHomeBase > self.DisengageRadius then self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) @@ -444,9 +443,13 @@ function AI_AIR:onafterStatus() if not self:Is( "Fuel" ) and not self:Is( "Home" ) then + local Fuel = self.Controllable:GetFuelMin() - self:F({Fuel=Fuel, FuelThresholdPercentage=self.FuelThresholdPercentage}) + + -- If the fuel in the controllable is below the treshold percentage, + -- then send for refuel in case of a tanker, otherwise RTB. if Fuel < self.FuelThresholdPercentage then + if self.TankerName then self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) self:Refuel() @@ -468,7 +471,10 @@ function AI_AIR:onafterStatus() -- TODO: Check GROUP damage function. local Damage = self.Controllable:GetLife() local InitialLife = self.Controllable:GetLife0() - self:F( { Damage = Damage, InitialLife = InitialLife, DamageThreshold = self.PatrolDamageThreshold } ) + + -- If the group is damaged, then RTB. + -- Note that a group can consist of more units, so if one unit is damaged of a group, the mission may continue. + -- The damaged unit will RTB due to DCS logic, and the others will continue to engage. if ( Damage / InitialLife ) < self.PatrolDamageThreshold then self:E( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) self:Damaged() @@ -489,6 +495,7 @@ function AI_AIR:onafterStatus() self:Damaged() else self:E( self.Controllable:GetName() .. " control lost! " ) + self:LostControl() end else diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index e6c6648fd..e67d61605 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -934,7 +934,7 @@ end -- @return #number The fuel state of the unit with the least amount of fuel -- @return #Unit reference to #Unit object for further processing function GROUP:GetFuelMin() - self:F(self.ControllableName) + self:F3(self.ControllableName) if not self:GetDCSObject() then BASE:E( { "Cannot GetFuel", Group = self, Alive = self:IsAlive() } ) diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index d24a0b0b0..f1e5ee77c 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -555,7 +555,7 @@ end -- @return #number The relative amount of fuel (from 0.0 to 1.0). -- @return #nil The DCS Unit is not existing or alive. function UNIT:GetFuel() - self:F( self.UnitName ) + self:F3( self.UnitName ) local DCSUnit = self:GetDCSObject() @@ -571,7 +571,7 @@ end -- @param #UNIT self -- @return #list A list of one @{Wrapper.Unit}. function UNIT:GetUnits() - self:F2( { self.UnitName } ) + self:F3( { self.UnitName } ) local DCSUnit = self:GetDCSObject() local Units = {} From 8969c58ca5ed8878159a397295d0fe11ab5b383a Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 12 Dec 2018 16:03:37 +0100 Subject: [PATCH 097/147] AIRBOS v0.5.2w --- Moose Development/Moose/Ops/Airboss.lua | 200 +++++++++++++++++------- 1 file changed, 143 insertions(+), 57 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 3d084eb50..90e5ea375 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -721,17 +721,13 @@ AIRBOSS.GroovePos={ -- @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 Rmin Minimum allowed range to carrier. --- @field #number Rmax Maximum allowed range to carrier. --- @field #number Amin Minimum allowed angle to carrier. --- @field #number Amax Maximum allowed angle 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. --- Parameters of a flight group. --- @type AIRBOSS.Flightitem +-- @type AIRBOSS.FlightGroup -- @field Wrapper.Group#GROUP group Flight group. -- @field #string groupname Name of the group. -- @field #number nunits Number of units in group. @@ -746,6 +742,14 @@ AIRBOSS.GroovePos={ -- @field #number case Recovery case of flight. -- @field #string seclead Name of section lead. -- @field #table section Other human flight groups belonging to this flight. This flight is the lead. +-- @field #boolean holding If true, flight is in holding zone. +-- @field #boolean ballcall If true, flight called the ball in the groove. +-- @field #table elements Flight group elements. + +--- Parameters of an element in a flight group. +-- @type AIRBOSS.FlightElement +-- @field Wrapper.Unit#UNIT unit Aircraft unit. +-- @field #string onboard Onboard number. -- @field #boolean ballcall If true, flight called the ball in the groove. --- Player data table holding all important parameters of each player. @@ -755,13 +759,12 @@ AIRBOSS.GroovePos={ -- @field Wrapper.Client#CLIENT client Client object of player. -- @field #string callsign Callsign of player. -- @field #string difficulty Difficulty level. --- @field #string step Coming pattern step. +-- @field #string step Current/next pattern step. -- @field #boolean warning Set true once the player got a warning. -- @field #number passes Number of passes. -- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. -- @field #table debrief Debrief analysis of the current step of this pass. -- @field #table grades LSO grades of player passes. --- @field #boolean holding If true, player is in holding zone. -- @field #boolean landed If true, player landed or attempted to land. -- @field #boolean boltered If true, player boltered. -- @field #boolean waveoff If true, player was waved off during final approach. @@ -772,7 +775,7 @@ AIRBOSS.GroovePos={ -- @field #number wire Wire caught by player when trapped. -- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. -- @field #table menu F10 radio menu --- @extends #AIRBOSS.Flightitem +-- @extends #AIRBOSS.FlightGroup --- Main radio menu. -- @field #table MenuF10 @@ -780,7 +783,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.2" +AIRBOSS.version="0.5.2w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1447,31 +1450,35 @@ function AIRBOSS:_GetACNickname(actype) return nickname end ---- Check recovery times and start/stop recovery mode of aircraft. +--- Check AI status. Pattern queue AI in the groove? Marshal queue AI arrived in holding zone? -- @param #AIRBOSS self function AIRBOSS:_CheckAIStatus() -- Loop over all flights in landing pattern. for _,_flight in pairs(self.Qpattern) do - local flight=_flight --#AIRBOSS.Flightitem + local flight=_flight --#AIRBOSS.FlightGroup -- Only AI! if flight.ai then - - -- Get unnits - local units=flight.group:GetUnits() - + -- Loop over all units in AI flight. - for _,_unit in pairs(units) do - local unit=_unit --Wrapper.Unit#UNIT + for _,_element in pairs(flight.elements) do + local element=_element --#AIRBOSS.FlightElement + + -- Unit + local unit=element.unit -- Get lineup and distance to carrier. local lineup=self:_Lineup(unit, true) - local distance=UTILS.NMToMeters(unit:GetCoordinate():Get2DDistance(self:GetCoordinate())) + + -- Distance in NM. + local distance=UTILS.MetersToNM(unit:GetCoordinate():Get2DDistance(self:GetCoordinate())) + + -- Altitude in ft. local alt=UTILS.MetersToFeet(unit:GetAltitude()) -- Check if parameters are right and flight is in the groove. - if lineup<2 and distance<=0.75 and alt<500 and not flight.ballcall then + if lineup<2 and distance<=0.75 and alt<500 and not element.ballcall then -- Paddles: Call the ball! self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.CALLTHEBALL, false, 0) @@ -1479,15 +1486,14 @@ function AIRBOSS:_CheckAIStatus() -- Pilot: "405, Hornet Ball, 3.2" -- TODO: Message to players only. -- TODO: Voice over. - -- TODO: Correct unit onboard number not section lead! - local text=string.format("%s, %s Ball, %.1f.", flight.onboard, self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) - MESSAGE:New(text, 5):ToCoalition(self:GetCoalition()) + local text=string.format("%s, %s Ball, %.1f.", element.onboard, self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) + MESSAGE:New(text, 15):ToCoalition(self:GetCoalition()) --self:MessageToPlayer(playerData, text, playerData.onboard, "", 3, false, 3) -- Paddles: Roger ball after 3 seconds. self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 3) - -- TODO: This does not work for flights with more than one aircraft in the group! But we need to set it not to flood the screen with messages for the same unit. + -- This is for the whole flight. Maybe we need it. flight.ballcall=true end @@ -1552,7 +1558,7 @@ function AIRBOSS:_CheckPlayerPatternDistance(player) -- Loop over all other flights in pattern. for _,_flight in pairs(self.Qpattern) do - local flight=_flight --#AIRBOSS.Flightitem + local flight=_flight --#AIRBOSS.FlightGroup -- Now we still need to loop over all units in the flight. for _,_unit in pairs(flight.group:GetUnits()) do @@ -1597,8 +1603,6 @@ function AIRBOSS:_CheckRecoveryTimes() -- Loop over all slots. for _,_recovery in pairs(self.recoverytimes) do local recovery=_recovery --#AIRBOSS.Recovery - - --if recovery.OVER==false then -- Get start/stop clock strings. local Cstart=UTILS.SecondsToClock(recovery.START) @@ -2289,7 +2293,7 @@ function AIRBOSS:_CheckQueue() if nmarshal>0 and npattern0 then -- Last flight group send to pattern. - local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.Flightitem + local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.FlightGroup -- Recovery case of pattern flight. pcase=patternflight.case @@ -2435,7 +2439,7 @@ function AIRBOSS:_ScanCarrierZone() -- Find flights that are not in CCA. local remove={} for _,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.Flightitem + local flight=_flight --#AIRBOSS.FlightGroup if insideCCA[flight.groupname]==nil then table.insert(remove, flight.group) end @@ -2490,7 +2494,7 @@ end --- Tell AI to orbit at a specified position at a specified alititude with a specified speed. -- @param #AIRBOSS self --- @param #AIRBOSS.Flightitem flight Flight group. +-- @param #AIRBOSS.FlightGroup flight Flight group. -- @param #number nstack Stack number of group. (Should be #self.Qmarshal+1 for new flight groups.) function AIRBOSS:_MarshalAI(flight, nstack) @@ -2523,6 +2527,30 @@ function AIRBOSS:_MarshalAI(flight, nstack) -- Waypoints array. local wp={} + -- Current position. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil ,Speed, {}, "Current Position") + + -- If flight has not arrived in the holding zone, we guide it there. + if not flight.holding then + + -- Get altitude and positions. + local Altitude, p1, p2=self:_GetMarshalAltitude(nstack, flight.case) + + if flight.case==1 then + -- TODO: Test & fine tune. + -- Waypoint in front of the carrier + wp[2]=self:GetCoordinate():Translate(UTILS.NMtoMeters(10), self:GetHeading()-45) + -- Enter pattern + wp[3]=self:GetCoordinate():Translate(UTILS.NMtoMeters(5), self:GetHeading()-90) + --TODO: waypoint task that sets flight.holing to true. + else + wp[2]=p1:WaypointAirTurningPoint(nil ,Speed, {}, "Entering Marshal Pattern") + -- TODO: waypoint task! + end + + end + + -- Set up waypoints including collapsing the stack. for stack=nstack, 1, -1 do @@ -2531,7 +2559,7 @@ function AIRBOSS:_MarshalAI(flight, nstack) -- Get altitude and positions. local Altitude, p1, p2=self:_GetMarshalAltitude(stack, flight.case) - -- Right CW pattern for CASE II/III. + -- Right CCW pattern for CASE II/III. local c1=nil --Core.Point#COORDINATE local c2=nil --Core.Point#COORDINATE local p0=nil --Core.Point#COORDINATE @@ -2566,7 +2594,7 @@ function AIRBOSS:_MarshalAI(flight, nstack) end - -- Landing waypoint. + -- Landing waypoint. (Done separately now). --wp[#wp+1]=Carrier:SetAltitude(250):WaypointAirLanding(Speed, self.airbase, nil, "Landing") -- Reinit waypoints. @@ -2597,7 +2625,7 @@ end --- Tell AI to land on the carrier. -- @param #AIRBOSS self --- @param #AIRBOSS.Flightitem flight Flight group. +-- @param #AIRBOSS.FlightGroup flight Flight group. function AIRBOSS:_LandAI(flight) -- Aircraft speed when flying the pattern. @@ -2690,7 +2718,7 @@ end --- Add a flight group to a specific marshal stack and to the marshal queue. -- @param #AIRBOSS self --- @param #AIRBOSS.Flightitem flight Flight group. +-- @param #AIRBOSS.FlightGroup flight Flight group. -- @param #number stack Marshal stack. This (re-)sets the flag value. function AIRBOSS:_AddMarshalGroup(flight, stack) @@ -2722,7 +2750,7 @@ end --- Check if marshal stack can be collapsed. -- If next in line is an AI flight, this is done. If human player is next, we wait for "Commence" via F10 radio menu command. -- @param #AIRBOSS self --- @param #AIRBOSS.Flightitem flight Flight to go to pattern. +-- @param #AIRBOSS.FlightGroup flight Flight to go to pattern. function AIRBOSS:_CheckCollapseMarshalStack(flight) -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence. @@ -2750,7 +2778,7 @@ end --- Collapse marshal stack. -- @param #AIRBOSS self --- @param #AIRBOSS.Flightitem flight Flight that left the marshal stack. +-- @param #AIRBOSS.FlightGroup flight Flight that left the marshal stack. -- @param #boolean nopattern If true, flight does not go to pattern. function AIRBOSS:_CollapseMarshalStack(flight, nopattern) self:I({flight=flight, nopattern=nopattern}) @@ -2781,7 +2809,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) mflight.flag:Set(mstack-1) -- Inform players. - if mflight.player and mflight.difficulty~=AIRBOSS.Difficulty.HARD then + if mflight.ai==false and mflight.difficulty~=AIRBOSS.Difficulty.HARD then local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(mstack-1,case)) local text=string.format("descent to next lower stack at %d ft", alt) self:MessageToPlayer(mflight, text, "MARSHAL") @@ -2892,7 +2920,7 @@ function AIRBOSS:_GetQueueInfo(queue, case) -- Loop over flight groups. for _,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Flightitem + local flight=_flight --#AIRBOSS.FlightGroup -- Check if a specific case was requested. if case then @@ -2929,7 +2957,7 @@ function AIRBOSS:_PrintQueue(queue, name) text=text.." empty." else for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Flightitem + local flight=_flight --#AIRBOSS.FlightGroup -- Timestamp. local clock=UTILS.SecondsToClock(flight.time) @@ -2978,14 +3006,14 @@ end --- Create a new flight group. Usually when a flight appears in the CCA. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. --- @return #AIRBOSS.Flightitem Flight group. +-- @return #AIRBOSS.FlightGroup Flight group. function AIRBOSS:_CreateFlightGroup(group) -- Debug info. self:I(self.lid..string.format("Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName())) -- New flight. - local flight={} --#AIRBOSS.Flightitem + local flight={} --#AIRBOSS.FlightGroup -- Check if not already in flights if not self:_InQueue(self.flights, group) then @@ -3012,6 +3040,19 @@ function AIRBOSS:_CreateFlightGroup(group) -- Note, this should be re-set elsewhere! flight.case=self.case + flight.elements={} + local units=group:GetUnits() + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + local name=unit:GetName() + local element={} --#AIRBOSS.FlightElement + element.unit=unit + element.onboard=flight.onboardnumbers[name] + element.ballcall=false + table.insert(flight.elements, element) + end + + -- Onboard if flight.ai then local onboard=flight.onboardnumbers[flight.seclead] @@ -3116,7 +3157,7 @@ end -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Group that will be removed from queue. -- @param #table queue The queue from which the group will be removed. --- @return #AIRBOSS.Flightitem Flight group. +-- @return #AIRBOSS.FlightGroup Flight group. -- @return #number Queue index. function AIRBOSS:_GetFlightFromGroupInQueue(group, queue) @@ -3125,7 +3166,7 @@ function AIRBOSS:_GetFlightFromGroupInQueue(group, queue) -- Loop over all flight groups in queue for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Flightitem + local flight=_flight --#AIRBOSS.FlightGroup if flight.groupname==name then return flight, i @@ -3136,6 +3177,43 @@ function AIRBOSS:_GetFlightFromGroupInQueue(group, queue) return nil, nil end +--- Get element in flight. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @return #AIRBOSS.FlightElement Flight element. +-- @return #number Element index. +function AIRBOSS:_GetFlightElement(unitname, flight) + + -- Loop over all elements in flight group. + for i,_element in pairs(flight) do + local element=_element --#AIRBOSS.FlightElement + + if element.unit:GetName()==unitname then + return element, i + end + end + + self:T2(self.lid..string.format("WARNING: Flight element %s could not be found in flight group.", unitname, flight.groupname)) + return nil, nil +end + +--- Get element in flight. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @param #AIRBOSS.FlightGroup flight Flight group. +function AIRBOSS:_RemoveFlightElement(unitname, flight) + + -- Get table index. + local element,idx=self:_GetFlightElement(unitname,flight) + + if idx then + table.remove(flight.elements, idx) + else + self:E("ERROR: Flight element could not be removed from flight group. Index=nil!") + end +end + --- Check if a group is in a queue. -- @param #AIRBOSS self -- @param #table queue The queue to check. @@ -3144,7 +3222,7 @@ end function AIRBOSS:_InQueue(queue, group) local name=group:GetName() for _,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Flightitem + local flight=_flight --#AIRBOSS.FlightGroup if name==flight.groupname then return true end @@ -3156,11 +3234,11 @@ end --- Remove a flight group. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. --- @return #AIRBOSS.Flightitem Flight group. +-- @return #AIRBOSS.FlightGroup Flight group. function AIRBOSS:_RemoveFlightGroup(group) local groupname=group:GetName() for i,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.Flightitem + local flight=_flight --#AIRBOSS.FlightGroup if flight.groupname==groupname then self:I(string.format("Removing flight group %s (not in CCA).", groupname)) table.remove(self.flights, i) @@ -3172,12 +3250,12 @@ end --- Remove a flight group from a queue. -- @param #AIRBOSS self -- @param #table queue The queue from which the group will be removed. --- @param #AIRBOSS.Flightitem flight Flight group that will be removed from queue. +-- @param #AIRBOSS.FlightGroup flight Flight group that will be removed from queue. function AIRBOSS:_RemoveFlightFromQueue(queue, flight) -- Loop over all flights in group. for i,_flight in pairs(queue) do - local qflight=_flight --#AIRBOSS.Flightitem + local qflight=_flight --#AIRBOSS.FlightGroup -- Check for name. if qflight.groupname==flight.groupname then @@ -3201,7 +3279,7 @@ function AIRBOSS:_RemoveGroupFromQueue(queue, group) -- Loop over all flights in group. for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.Flightitem + local flight=_flight --#AIRBOSS.FlightGroup -- Check for name. if flight.groupname==name then @@ -3233,10 +3311,16 @@ function AIRBOSS:_RemoveUnitFromFlight(unit) -- Check if flight exists. if flight then - -- TODO: Improve this and remove the explicit unit. Make unit array for flight! + -- Remove element from flight group. + self:_RemoveFlightElement(unit:GetName(), flight) -- Decrease number of units in group. flight.nunits=flight.nunits-1 + + -- Check if numbers still match. + if #flight.elements~=flight.nunits then + self:E("ERROR: Number of elements != number of units in flight!") + end -- Check if no units are left. if flight.nunits==0 then @@ -3260,12 +3344,12 @@ function AIRBOSS:_RemoveFlight(flight) self:_RemoveFlightFromQueue(self.Qpattern, flight) -- Check if player or AI - if flight.player then - -- Set Playerstep to undefined. - flight.step=AIRBOSS.PatternStep.UNDEFINED - else + if flight.ai then -- Remove AI flight completely. self:_RemoveFlightFromQueue(self.flights, flight) + else + -- Set Playerstep to undefined. + flight.step=AIRBOSS.PatternStep.UNDEFINED end end @@ -3348,7 +3432,7 @@ function AIRBOSS:_CheckPatternUpdate() -- Loop over all marshal flights for _,_flight in pairs(self.Qmarshal) do - local flight=_flight --#AIRBOSS.Flightitem + local flight=_flight --#AIRBOSS.FlightGroup -- Update marshal pattern of AI keeping the same stack. if flight.ai then @@ -3776,7 +3860,7 @@ function AIRBOSS:_Holding(playerData) local playeralt=unit:GetAltitude() -- Get holding zone of player. - local zoneHolding=self:_GetZoneHolding(playerData.case, playerData.flag:Get()) + local zoneHolding=self:_GetZoneHolding(playerData.case, stack) -- Check if player is in holding zone. local inholdingzone=unit:IsInZone(zoneHolding) @@ -7143,6 +7227,8 @@ function AIRBOSS:_RequestMarshal(_unitName) self:MessageToPlayer(playerData, text, "MARSHAL") else + + -- TODO: check if recovery window is open. -- Add flight to marshal stack. self:_MarshalPlayer(playerData) @@ -7335,7 +7421,7 @@ function AIRBOSS:_SetSection(_unitName) -- Loop over all registered flights. for _,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.Flightitem + local flight=_flight --#AIRBOSS.FlightGroup -- Only human flight groups excluding myself. if flight.ai==false and flight.groupname~=playerData.groupname then From fa08434690c1ae88b5adf7b588861199b54644ba Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 12 Dec 2018 23:38:48 +0100 Subject: [PATCH 098/147] AIRBOSS v0.5.3 --- Moose Development/Moose/Ops/Airboss.lua | 234 ++++++++++++++++-------- 1 file changed, 154 insertions(+), 80 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 90e5ea375..a95572b36 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -194,7 +194,7 @@ -- @field #AIRBOSS AIRBOSS = { ClassName = "AIRBOSS", - Debug = true, + Debug = false, lid = nil, carrier = nil, carriertype = nil, @@ -583,7 +583,7 @@ AIRBOSS.MarshalCall={ subtitle="Marshal, radio check", duration=1.0, }, --- TODO: Other voice overs for marshal. + -- TODO: Other voice overs for marshal. N0={ file="LSO-N0", suffix="ogg", @@ -749,7 +749,8 @@ AIRBOSS.GroovePos={ --- Parameters of an element in a flight group. -- @type AIRBOSS.FlightElement -- @field Wrapper.Unit#UNIT unit Aircraft unit. --- @field #string onboard Onboard number. +-- @field #boolean ai If true, AI sits inside. If false, human player is flying. +-- @field #string onboard Onboard number of the aircraft. -- @field #boolean ballcall If true, flight called the ball in the groove. --- Player data table holding all important parameters of each player. @@ -783,18 +784,18 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.2w" +AIRBOSS.version="0.5.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: First send AI to marshal and then allow them into the landing pattern ==> task function when reaching the waypoint. -- TODO: PWO during case 2/3. Also when too close to other player. -- TODO: Option to filter AI groups for recovery. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! -- TODO: Foul deck check. -- TODO: Persistence of results. +-- DONE: First send AI to marshal and then allow them into the landing pattern ==> task function when reaching the waypoint. -- DONE: Extract (static) weather from mission for cloud covery etc. -- DONE: Check distance to players during approach. -- DONE: Option to turn AI handling off. @@ -1493,6 +1494,9 @@ function AIRBOSS:_CheckAIStatus() -- Paddles: Roger ball after 3 seconds. self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 3) + -- Flight element called the ball. + element.ballcall=true + -- This is for the whole flight. Maybe we need it. flight.ballcall=true end @@ -1561,6 +1565,7 @@ function AIRBOSS:_CheckPlayerPatternDistance(player) local flight=_flight --#AIRBOSS.FlightGroup -- Now we still need to loop over all units in the flight. + -- TODO: Replace by elements. for _,_unit in pairs(flight.group:GetUnits()) do -- Check if player is too close to another aircraft in the pattern. @@ -1781,8 +1786,8 @@ end -- Parameter initialization ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Function called when group is passing a waypoint. ---@param Wrapper.Group#GROUP Group that passed the waypoint +--- Function called when a group is passing a waypoint. +--@param Wrapper.Group#GROUP group Group that passed the waypoint --@param #AIRBOSS airboss Airboss object. --@param #number i Waypoint number that has been reached. --@param #number final Final waypoint number. @@ -1802,6 +1807,32 @@ function AIRBOSS._PassingWaypoint(group, airboss, i, final) airboss.currentwp=i end +--- Function called when a group has reached the holding zone. +--@param Wrapper.Group#GROUP group Group that reached the holding zone. +--@param #AIRBOSS airboss Airboss object. +--@param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone. +function AIRBOSS._ReachedHoldingZone(group, airboss, flight) + + -- Debug message. + local text=string.format("Group %s has reached the holding zone.", group:GetName()) + + local pos=group:GetCoordinate() + pos:SmokeRed() + local MarkerID=pos:MarkToAll(string.format("Flight group %s reached holding zone.", group:GetName())) + + MESSAGE:New(text,10):ToAll() + env.info(text) + + -- Set current waypoint. + --local flight=airboss:_GetFlightFromGroupInQueue(group, airboss.flights) + + -- Set holding flag true and set timestamp for marshal time check. + if flight then + flight.holding=true + flight.time=timer.getAbsTime() + end +end + --- Patrol carrier -- @param #AIRBOSS self @@ -1957,7 +1988,7 @@ function AIRBOSS:_InitStennis() self.carrierparam.wire2 = 12 self.carrierparam.wire3 = 24 self.carrierparam.wire4 = 36 - self.carrierparam.wireoffset = 30 + self.carrierparam.wireoffset = 50 -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight. @@ -2274,6 +2305,33 @@ end -- QUEUE Functions ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get next marshal flight which is ready to enter the landing pattern. +-- @param #AIRBOSS self +-- @return #AIRBOSS.FlightGroup Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready. +function AIRBOSS:_GetNextMarshalFight() + + -- Min 5 min in marshal before send to landing pattern. + local TmarshalMin=5*60 + + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Current stack. + local stack=flight.flag:Get() + + -- Marshal time. + local Tmarshal=timer.getAbsTime()-flight.time + + -- Check if conditions are right. + if stack==1 and flight.holding and Tmarshal>=TmarshalMin then + return flight + end + end + + return nil +end + + --- Check marshal and pattern queues. -- @param #AIRBOSS self function AIRBOSS:_CheckQueue() @@ -2289,15 +2347,14 @@ function AIRBOSS:_CheckQueue() -- Get number of flight groups(!) in marshal pattern. local nmarshal,_=self:_GetQueueInfo(self.Qmarshal) - -- Check if there are flights in marshal strack and if the pattern is free. - if nmarshal>0 and npattern45 sec interval between pattern flights. - if self:IsRecovering() and Tmarshal>TmarshalMin and Tpattern>TpatternMin then + -- Check recovery window open and enough space to last pattern flight. + if self:IsRecovering() and Tpattern>TpatternMin then self:_CheckCollapseMarshalStack(marshalflight) end @@ -2511,15 +2565,18 @@ function AIRBOSS:_MarshalAI(flight, nstack) -- Current carrier position. local Carrier=self:GetCoordinate() - -- Aircraft speed when flying the pattern. - local Speed=UTILS.KnotsToMps(272) + -- Aircraft speed 272 knots when orbiting the pattern. (Orbit expects m/s.) + local SpeedOrbit=UTILS.KnotsToMps(272) + + -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) + local SpeedTransit=UTILS.KnotsToKmph(400) --- Create a DCS task to orbit at a certain altitude. local function _taskorbit(p1, alt, speed, stopflag, p2) local DCSTask={} DCSTask.id="ControlledTask" DCSTask.params={} - DCSTask.params.task=group:TaskOrbit(p1, alt, speed, p2) + DCSTask.params.task=group:TaskOrbit(p1, alt, speed, p2) DCSTask.params.stopCondition={userFlag=groupname, userFlagValue=stopflag} return DCSTask end @@ -2527,25 +2584,29 @@ function AIRBOSS:_MarshalAI(flight, nstack) -- Waypoints array. local wp={} - -- Current position. - wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil ,Speed, {}, "Current Position") + -- Current position. Not sure if necessary but might be. Need to test if it hurts or not. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, SpeedTransit, {}, "Current Position") -- If flight has not arrived in the holding zone, we guide it there. if not flight.holding then -- Get altitude and positions. - local Altitude, p1, p2=self:_GetMarshalAltitude(nstack, flight.case) + local Altitude, p1, p2=self:_GetMarshalAltitude(nstack, flight.case) + + -- Task function when arriving at the holding zone. This will set flight.holding=true. + local TaskArrivedHolding=flight.group:TaskFunction("AIRBOSS._ReachedHoldingZone", self, flight) + + -- Carrier heading. + local hdg=self:GetHeading() if flight.case==1 then - -- TODO: Test & fine tune. - -- Waypoint in front of the carrier - wp[2]=self:GetCoordinate():Translate(UTILS.NMtoMeters(10), self:GetHeading()-45) - -- Enter pattern - wp[3]=self:GetCoordinate():Translate(UTILS.NMtoMeters(5), self:GetHeading()-90) - --TODO: waypoint task that sets flight.holing to true. + -- Waypoint "north" of carrier's holding zone. + wp[2]=p1:Translate(UTILS.NMToMeters(10), hdg):WaypointAirTurningPoint(nil, SpeedTransit, {}, "Prepare Entering Case I Marshal Pattern") + -- Enter pattern from "north" to "south". + wp[3]=p1:Translate( UTILS.NMToMeters(5), hdg):WaypointAirTurningPoint(nil, SpeedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") else - wp[2]=p1:WaypointAirTurningPoint(nil ,Speed, {}, "Entering Marshal Pattern") - -- TODO: waypoint task! + -- TODO: Test and tune! + wp[2]=p1:WaypointAirTurningPoint(nil, SpeedTransit, {TaskArrivedHolding}, "Entering Marshal Pattern") end end @@ -2559,13 +2620,13 @@ function AIRBOSS:_MarshalAI(flight, nstack) -- Get altitude and positions. local Altitude, p1, p2=self:_GetMarshalAltitude(stack, flight.case) - -- Right CCW pattern for CASE II/III. + -- Correct CCW pattern for CASE II/III. local c1=nil --Core.Point#COORDINATE local c2=nil --Core.Point#COORDINATE local p0=nil --Core.Point#COORDINATE if flight.case==1 then c1=p1 - p0=self:GetCoordinate() + p0=self:GetCoordinate():Translate(UTILS.NMToMeters(5), -90):SetAltitude(Altitude) else c1=p2 c2=p1 @@ -2576,10 +2637,10 @@ function AIRBOSS:_MarshalAI(flight, nstack) local Dist=p1:Get2DDistance(self:GetCoordinate()) -- Task: orbit at specified position, altitude and speed until flag=stack-1 - local TaskOrbit=_taskorbit(c1, Altitude, Speed, stack-1, c2) + local TaskOrbit=_taskorbit(c1, Altitude, SpeedOrbit, stack-1, c2) -- Waypoint description. - local text=string.format("Flight %s: Marshal stack %d: alt=%d, dist=%.1f, speed=%d", flight.groupname, stack, UTILS.MetersToFeet(Altitude), UTILS.MetersToNM(Dist), UTILS.MpsToKnots(Speed)) + local text=string.format("Flight %s: Marshal stack %d: alt=%d, dist=%.1f, speed=%d", flight.groupname, stack, UTILS.MetersToFeet(Altitude), UTILS.MetersToNM(Dist), UTILS.MpsToKnots(SpeedOrbit)) -- Debug mark. if self.Debug then @@ -2590,7 +2651,7 @@ function AIRBOSS:_MarshalAI(flight, nstack) end -- Waypoint. - wp[#wp+1]=p0:SetAltitude(Altitude):WaypointAirTurningPoint(nil, Speed, {TaskOrbit}, text) + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, SpeedTransit, {TaskOrbit}, text) end @@ -2604,39 +2665,20 @@ function AIRBOSS:_MarshalAI(flight, nstack) group:Route(wp, 0) end ---- Task function. --- @param #AIRBOSS self -function AIRBOSS:_InitPatternTaskFunction() - - -- Name of the warehouse (static) object. - local carriername=self.carrier:GetName() - - -- Task script. - local DCSScript = {} - DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername) -- The carrier unit that holds the self object. - DCSScript[#DCSScript+1] = string.format('local myairboss = mycarrier:GetState(mycarrier, \"AIRBOSS\") ') -- Get the AIRBOSS self object. - DCSScript[#DCSScript+1] = string.format('myairboss:PatternUpdate()') -- Call the function, e.g. mytanker.(self) - - -- Create task. - local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) - - return DCSTask -end - --- Tell AI to land on the carrier. -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight group. function AIRBOSS:_LandAI(flight) -- Aircraft speed when flying the pattern. - local Speed=UTILS.KnotsToMps(272) + local Speed=UTILS.KnotsToKmph(272) local Carrier=self:GetCoordinate() -- Waypoints array. local wp={} - wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil ,Speed, {}, "Current position") + wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, Speed, {}, "Current position") -- Landing waypoint. wp[#wp+1]=self:GetCoordinate():SetAltitude(250):WaypointAirLanding(Speed, self.airbase, nil, "Landing") @@ -2646,7 +2688,6 @@ function AIRBOSS:_LandAI(flight) -- Route group. flight.group:Route(wp, 0) - end --- Get marshal altitude and position. @@ -2713,6 +2754,12 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) -- Pattern altitude. local altitude=UTILS.FeetToMeters(((stack-1)+angels0)*1000) + -- Set altitude of coordinate. + p1:SetAltitude(altitude, true) + if p2 then + p2:SetAltitude(altitude, true) + end + return altitude, p1, p2 end @@ -2801,7 +2848,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- Only collapse stacks above the new pattern flight. -- This will go wrong, if patternflight is not in marshal stack because it will have value -100 and all mstacks will be larger! - -- Maybe need to set the initial value to 1000? Or check pstack>0? + -- Maybe need to set the initial value to 1000? Or check stack>0 of pattern flight? if stack>0 and mstack>stack then -- Decrease stack/flag by one ==> AI will go lower. @@ -2928,7 +2975,7 @@ function AIRBOSS:_GetQueueInfo(queue, case) -- Only count specific case with special 23 = CASE II and III combined. if (flight.case==case) or (case==23 and (flight.case==2 or flight.case==3)) then ngroup=ngroup+1 - nunits=nunits+flight.nunits + nunits=nunits+flight.nunits end else @@ -3040,18 +3087,22 @@ function AIRBOSS:_CreateFlightGroup(group) -- Note, this should be re-set elsewhere! flight.case=self.case + -- Flight elements. + local text=string.format("Flight elemets of group %s:", flight.groupname) flight.elements={} local units=group:GetUnits() - for _,_unit in pairs(units) do + for i,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT local name=unit:GetName() local element={} --#AIRBOSS.FlightElement element.unit=unit element.onboard=flight.onboardnumbers[name] element.ballcall=false + --element.ai= + text=text..string.format("\n[%d] %s onboard #%s", i, name, tostring(element.onboard)) table.insert(flight.elements, element) end - + self:I(self.lid..text) -- Onboard if flight.ai then @@ -3186,7 +3237,7 @@ end function AIRBOSS:_GetFlightElement(unitname, flight) -- Loop over all elements in flight group. - for i,_element in pairs(flight) do + for i,_element in pairs(flight.elements) do local element=_element --#AIRBOSS.FlightElement if element.unit:GetName()==unitname then @@ -4790,18 +4841,20 @@ function AIRBOSS:_GetWire(Ccoord, Lcoord, dx) -- Little offset for the exact wire positions. dx=dx or self.carrierparam.wireoffset + dx=self.carrierparam.wireoffset + -- Corrected distance. - local d=Ldist+dx + local d=Ldist-dx -- Which wire was caught? X>0 since calculated as distance! local wire - if d wire=%d.", Ldist, dx, d, wire)) + self:I(string.format("GetWire: L=%.1f m, dx=%.1f m, d=L-dx=%.1f m ==> wire=%d.", Ldist, dx, d, wire)) return wire end @@ -6491,17 +6546,36 @@ function AIRBOSS:_IsCarrierAircraft(unit) return false end +--- Checks if a human player sits in the unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #boolean If true, human player inside the unit. +function AIRBOSS:_IsHumanUnit(unit) + + -- Get player unit or nil if no player unit. + local playerunit=self:_GetPlayerUnitAndName(unit:GetName()) + + if playerunit then + return true + else + return false + end +end + --- Checks if a group has a human player. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. -- @return #boolean If true, human player inside group. function AIRBOSS:_IsHuman(group) + -- Get all units of the group. local units=group:GetUnits() + -- Loop over all units. for _,_unit in pairs(units) do - local playerunit=self:_GetPlayerUnitAndName(_unit:GetName()) - if playerunit then + -- Check if unit is human. + local human=self:_IsHumanUnit(_unit) + if human then return true end end From 07f313a4a685ba7804a4a4a537fa8bbe14e8119f Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 13 Dec 2018 16:08:59 +0100 Subject: [PATCH 099/147] AIRBOSS v0.5.3w --- Moose Development/Moose/Ops/Airboss.lua | 257 +++++++++++++++++++----- 1 file changed, 209 insertions(+), 48 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index a95572b36..f98e358ea 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -22,6 +22,19 @@ -- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much **work in progress**. -- Your constructive feedback is both necessary and highly appreciated. -- +-- Supported Carriers: +-- +-- * USS John C. Stennis: +-- +-- Supported Player and AI Aircraft: +-- +-- * F/A-18C Hornet Lot 20 (player+AI) +-- * A-4E-C community mod (player+AI) +-- * F/A-18C (AI) +-- * F-14A (AI) +-- * E-2D (AI) +-- * S-3B (AI) +-- -- At the moment, parameters are optimized for F/A-18C Hornet as aircraft and USS John C. Stennis as carrier. -- The community A-4E mod is also supported in priciple but maybe needs further tweaking of parameters such as on speed AoA values. -- @@ -31,8 +44,8 @@ -- -- ### Author: **funkyfranky** -- ### Special thanks to --- **Bankler** for his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script! --- This gave the inspiration for this class. Also this uses some functionalities for determining the player positon in Case I recoveries. +-- **Bankler** for his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script! +-- This gave the inspiration for this class. Also this class uses some functionalities for determining the player positon in Case I recoveries he developed. -- -- @module Ops.Airboss -- @image MOOSE.JPG @@ -189,7 +202,139 @@ -- Note that incoming flights will be assigned a holding pattern for the next opening window case if no window is open at the moment. So in the above example, -- all flights incoming after 13:15 will be assigned to a Case III marshal stack. Therefore, you should make sure that no flights are incoming long before the -- next window opens or adjust the recovery planning accordingly. --- +-- +-- # The F10 Radio Menu +-- +-- The F10 radio menu can be used to post requests to Marshal but also provides information about the player and carrier status. Additionally, helper functions +-- can be called. +-- +-- ## Main Menu +-- +-- The general structure +-- +-- * **F1 Help...**: Help submenu, see below. +-- * **F2 Kneeboard...**: Kneeboard submenu, see below. Carrier information, weather report, player status. +-- * **F3 Request Marshal** +-- * **F4 Request Commence** +-- * **F5 Request Refueling** +-- +-- ### Request Marshal +-- +-- This radio command can be used to request a stack in the holding pattern from Marshal. Necessary conditions are that the flight is inside the CCZ. +-- Marshal will assign an individual stack for each player group depending on the current or next open recovery case window. +-- If multiple players have registered as a section, the section lead will be assigned a stack and is responsible to guide his section to the assigned holding position. +-- +-- ### Request Commence +-- +-- This command can be used to request commencing from the marshal stack to the landing pattern. Necessary condition is that the player is in the lowest marshal stack +-- and that the number of aircraft in the landing pattern is smaller than four. +-- +-- A player can also request commencing if he is not registered in a marshal stack yet. If the pattern is free, Marshal will allow him to directly enter the landing pattern. +-- +-- ### Request Refueling +-- +-- If a recovery tanker was setup via the @{#AIRBOSS.SetRecoveryTanker} function, the player can request refueling. If the tanker is ready, refueling is granted and the player +-- can leave the marshal stack for refueling. The stack will collapse and the player needs to request marshal again, when refueling is finished. +-- +-- ## Help Menu +-- +-- This menu provides commands to help the player. +-- +-- ### Skill Level Submenu +-- +-- The player can choose between three skill or difficulty levels. +-- +-- * **Flight Student**: The player receives tips at certain stages of the pattern, e.g. if he is at the right altitude, speed, etc. +-- * **Naval Aviator**: Less tips are show. Player should be familiar with the procedures and its aircraft parameters. +-- * **TOPGUN Graduate**: Only very few information is provided to the player. This is for pros. +-- +-- ### Mark Zones Submenu +-- +-- These commands can be used to mark marshal or landing pattern zones. +-- +-- * **Smoke My Marshal Zone** This smokes the the surrounding area of the currently assigned marshal zone of the player. Player has to be registered for marshal. +-- * **Flare My Marshal Zone** Similar to smoke but uses flares to mark the marshal zone. +-- * **Smoke Pattern Zones** Smoke is used to mark the landing pattern zone of the player depending on his recovery case. +-- For Case I this is the initial zone. For Case II/III and three these are the Platform, Arc turn, Dirty Up, Bullseye/Initial zones as well as the approach corridor. +-- * **Flare Pattern Zones** Similar to smoke but uses flares to mark the pattern zones. +-- +-- ### My Status +-- +-- This command provides information about the current player status. For example, his current step in the pattern. +-- +-- ### Attitude Monitor +-- +-- This command displays the current aircraft attitude of the player in short intervals as message on the screen. +-- It provides information about current pitch, roll, yaw, lineup and glideslope error, orientation of the plane wrt to carrier etc. +-- +-- ### LSO Radio Check +-- +-- LSO will transmit a short message on his radio frequency. See @{#AIRBOSS.SetLSORadio}. +-- +-- ### Marshal Radio Check +-- +-- Marshal will transmit a short message on his radio frequency. See @{#AIRBOSS.SetMarshalRadio}. +-- +-- ### [Reset My Status] +-- +-- This will reset the current player status. If player is currently in a marshal stack, he will be removed from the marshal queue and the stack will collapse. +-- The player needs to re-register later if desired. If player is currently in the landing pattern, he will be removed from the pattern queue. +-- +-- ## Kneeboard Menu +-- +-- The Kneeboard menu provides information about the carrier, weather and player results. +-- +-- ### Results Submenu +-- +-- Here you find your LSO grading results as well as scores of other players. +-- +-- * **Greenie Board** lists average scores of all players obtained during landing approaches. +-- * **My LSO Grades** lists all grades the player has received for his approaches in this mission. +-- * **Last Debrief** shows the detailed debriefing of the player's last approach. +-- +-- ### Carrier Info +-- +-- Information about the current carrier status is displayed. This includes current BRC, FB, LSO and Marshal frequences, list of next recovery windows. +-- +-- ### Weather Report +-- +-- Displays information about the current weather at the carrier such as QFE, wind and temperature. +-- +-- ### Set Section +-- +-- With this command, you can define a section of human flights. The player how issues the command becomes the section lead and all other human players +-- within a radius of 200 meters become members of the section. +-- +-- # Landing Signal Officer (LSO) +-- +-- The LSO will first contact you on his radio channel when you are at the the abeam position (Case I) with the phrase "Paddles, contact.". +-- Once you are in the groove the LSO will ask you to "Call the ball." and then acknoledge your ball call by "Roger Ball." +-- +-- During the groove the LSO will give you advice if you deviate from the correct landing path. These advices will be given when you are +-- +-- * too low or too high with respect to the glideslope, +-- * too fast or too slow with respect to the optimal AoA, +-- * too far left or too far right wirth respect to the lineup of the (angled) runway. +-- +-- ## LSO Grading +-- +-- LSO grading starts when the player enters the groove. The flight path and aircraft attitude is evaluated at certain steps +-- +-- * **X** At the Start +-- * **IM** In the Middle +-- * **IC** In Close +-- * **AR** At the Ramp +-- * **IW** In the Wires +-- +-- Grading at each step includes the above calls, i.e. +-- +-- * Linup: (LUL), LUL, _LUL_, (RUL), RUL, _RUL_ +-- * Alitude: (H), H, _H_, (L), L, _L_ +-- * Speed: (F), F, _F_, (S), S, _S_ +-- +-- The position at the landing even is analyses and the corresponding trapped wire calculated. If no wire was caught, the LSO will give the bolter call. +-- +-- If a player is sigifiantly off from the ideal parameters in close or at the ramp, the LSO will wave off the player. -- -- @field #AIRBOSS AIRBOSS = { @@ -784,7 +929,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.3" +AIRBOSS.version="0.5.3w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1565,16 +1710,16 @@ function AIRBOSS:_CheckPlayerPatternDistance(player) local flight=_flight --#AIRBOSS.FlightGroup -- Now we still need to loop over all units in the flight. - -- TODO: Replace by elements. - for _,_unit in pairs(flight.group:GetUnits()) do + for _,_element in pairs(flight.elements) do + local element=_element --#AIRBOSS.FlightElement -- Check if player is too close to another aircraft in the pattern. - local tooclose=_checkclose(player, _unit) + local tooclose=_checkclose(player.unit, element.unit) if tooclose then - local text=string.format("Player %s too close (<200 meters) to aircraft %s!", player.name, _unit:GetName()) + local text=string.format("Player %s too close (<200 meters) to aircraft %s!", player.name, element.unit:GetName()) MESSAGE:New(text, 20, "DEBUG"):ToAllIf(self.Debug) - -- TODO: AIRBOSS call ==> Pattern wave off. + -- TODO: AIRBOSS call ==> Pattern wave off. end end @@ -1752,13 +1897,10 @@ end -- @param #string Event Event. -- @param #string To To state. function AIRBOSS:onafterRecoveryStop(From, Event, To) - -- Debug output. - self:I(self.lid..string.format("Stopping aircraft recovery. Carrier goes to state idle.")) - + self:I(self.lid..string.format("Stopping aircraft recovery. Carrier goes to state idle.")) end - --- On after "Idle" event. Carrier goes to state "Idle". -- @param #AIRBOSS self -- @param #string From From state. @@ -1769,7 +1911,6 @@ function AIRBOSS:onafterIdle(From, Event, To) self:I(self.lid..string.format("Carrier goes to idle.")) end - --- On after Stop event. Unhandle events. -- @param #AIRBOSS self -- @param #string From From state. @@ -1796,12 +1937,14 @@ function AIRBOSS._PassingWaypoint(group, airboss, i, final) -- Debug message. local text=string.format("Group %s passing waypoint %d of %d.", group:GetName(), i, final) - local pos=group:GetCoordinate() - pos:SmokeRed() - local MarkerID=pos:MarkToAll(string.format("Reached Waypoint %d of group %s", i, group:GetName())) + if airboss.Debug then + local pos=group:GetCoordinate() + pos:SmokeRed() + local MarkerID=pos:MarkToAll(string.format("Group %s reached waypoint %d", group:GetName(), i)) + end - MESSAGE:New(text,10):ToAll() - env.info(text) + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) -- Set current waypoint. airboss.currentwp=i @@ -1816,15 +1959,15 @@ function AIRBOSS._ReachedHoldingZone(group, airboss, flight) -- Debug message. local text=string.format("Group %s has reached the holding zone.", group:GetName()) - local pos=group:GetCoordinate() - pos:SmokeRed() - local MarkerID=pos:MarkToAll(string.format("Flight group %s reached holding zone.", group:GetName())) - - MESSAGE:New(text,10):ToAll() - env.info(text) - - -- Set current waypoint. - --local flight=airboss:_GetFlightFromGroupInQueue(group, airboss.flights) + -- Debug mark. + if airboss.Debug then + local pos=group:GetCoordinate() + local MarkerID=pos:MarkToAll(string.format("Flight group %s reached holding zone.", group:GetName())) + end + + -- Message output + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) -- Set holding flag true and set timestamp for marshal time check. if flight then @@ -3529,10 +3672,16 @@ function AIRBOSS:_CheckPlayerStatus() if unit:IsInZone(self.zoneCCA) then -- Check if player is too close to another aircraft in the pattern. - -- TODO: Find a better place to call this! - --self:_CheckPlayerPatternDistance(playerData) - local Tnow=timer.getTime() - env.info(string.format("T=%s step=%s", Tnow, playerData.step)) + -- TODO: At which steps is the really necessary. Case II/III? + if playerData.step==AIRBOSS.PatternStep.INITIAL or + playerData.step==AIRBOSS.PatternStep.BREAKENTRY or + playerData.step==AIRBOSS.PatternStep.EARLYBREAK or + playerData.step==AIRBOSS.PatternStep.LATEBREAK or + playerData.step==AIRBOSS.PatternStep.ABEAM or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM then + self:_CheckPlayerPatternDistance(playerData) + end if playerData.step==AIRBOSS.PatternStep.UNDEFINED then @@ -8009,37 +8158,49 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) if flare then -- Case I/II: Initial - text=text.."* initial with WHITE flares\n" - self.zoneInitial:FlareZone(FLARECOLOR.White, 45) + if case==1 or case==2 then + text=text.."* initial with WHITE flares\n" + self.zoneInitial:FlareZone(FLARECOLOR.White, 45) + end -- Case II/III: approach corridor - text=text.."* approach corridor with GREEN flares\n" - self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) + if case==2 or case==3 then + text=text.."* approach corridor with GREEN flares\n" + self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) + end -- Case II/III: platform - text=text.."* platform with RED flares\n" - self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red, 45) + if case==2 or case==3 then + text=text.."* platform with RED flares\n" + self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red, 45) + end -- Case III: dirty up - text=text.."* dirty up with YELLOW flares\n" - self:_GetZoneDirtyUp(case):FlareZone(FLARECOLOR.Yellow, 45) + if case==3 then + text=text.."* dirty up with YELLOW flares\n" + self:_GetZoneDirtyUp(case):FlareZone(FLARECOLOR.Yellow, 45) + end -- Case II/III: arc in/out - if math.abs(self.holdingoffset)>0 then - self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.Yellow, 45) - text=text.."* arc turn in with YELLOW flares\n" - self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White, 45) - text=text.."* arc trun out with WHITE flares\n" + if case==2 or case==3 then + if math.abs(self.holdingoffset)>0 then + self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.Yellow, 45) + text=text.."* arc turn in with YELLOW flares\n" + self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White, 45) + text=text.."* arc trun out with WHITE flares\n" + end end -- Case III: bullseye - text=text.."* bullseye with WHITE flares\n" - self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.White, 45) + if case==3 then + text=text.."* bullseye with WHITE flares\n" + self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.White, 45) + end else -- Case I/II: Initial - if case==1 or case==2 then + if case==1 or case==2 then text=text.."* initial with WHITE smoke\n" self.zoneInitial:SmokeZone(SMOKECOLOR.White, 45) end From 5201c73d35ea8a5195dbbb89bc846e6b03ecb5ca Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 14 Dec 2018 00:17:11 +0100 Subject: [PATCH 100/147] AIRBOSS v0.5.4 --- Moose Development/Moose/Ops/Airboss.lua | 83 ++++++++++++------- .../Moose/Ops/RecoveryTanker.lua | 14 ++-- Moose Development/Moose/Utilities/Utils.lua | 2 +- 3 files changed, 60 insertions(+), 39 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index f98e358ea..9bc4befd6 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -400,18 +400,18 @@ AIRBOSS = { --- Player aircraft types capable of landing on carriers. -- @type AIRBOSS.AircraftPlayer --- @field #string AV8B AV-8B Night Harrier. +-- @field #string AV8B AV-8B Night Harrier (not yet supported). -- @field #string HORNET F/A-18C Lot 20 Hornet. -- @field #string A4EC Community A-4E-C mod. AIRBOSS.AircraftPlayer={ - AV8B="AV8BNA", + --AV8B="AV8BNA", HORNET="FA-18C_hornet", A4EC="A-4E-C", } --- Aircraft types capable of landing on carrier (human+AI). -- @type AIRBOSS.AircraftCarrier --- @field #string AV8B AV-8B Night Harrier. +-- @field #string AV8B AV-8B Night Harrier (not yet supported). -- @field #string HORNET F/A-18C Lot 20 Hornet. -- @field #string A4EC Community A-4E mod. -- @field #string S3B Lockheed S-3B Viking. @@ -420,7 +420,7 @@ AIRBOSS.AircraftPlayer={ -- @field #string FA18C F/A-18C Hornet (AI). -- @field #string F14A F-14A (AI). AIRBOSS.AircraftCarrier={ - AV8B="AV8BNA", + --AV8B="AV8BNA", HORNET="FA-18C_hornet", A4EC="A-4E-C", S3B="S-3B", @@ -929,7 +929,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.3w" +AIRBOSS.version="0.5.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1696,6 +1696,8 @@ function AIRBOSS:_CheckPlayerPatternDistance(player) -- Get angle between the two orientation vectors. Does the player aircraft nose point into the direction of the other aircraft? (Could be behind him!) local rhdg=math.deg(math.acos(UTILS.VecDot(vec12,vec1)/UTILS.VecNorm(vec12)/UTILS.VecNorm(vec1))) + -- TODO: Check altitude difference? + -- Direction in 30 degrees cone and distance < 200 meters. -- TODO: Test parameter values. if math.abs(rhdg)<30 and dist<200 then @@ -2744,17 +2746,16 @@ function AIRBOSS:_MarshalAI(flight, nstack) if flight.case==1 then -- Waypoint "north" of carrier's holding zone. - wp[2]=p1:Translate(UTILS.NMToMeters(10), hdg):WaypointAirTurningPoint(nil, SpeedTransit, {}, "Prepare Entering Case I Marshal Pattern") + --wp[2]=p1:Translate(UTILS.NMToMeters(10), hdg):WaypointAirTurningPoint(nil, SpeedTransit, {}, "Prepare Entering Case I Marshal Pattern") -- Enter pattern from "north" to "south". - wp[3]=p1:Translate( UTILS.NMToMeters(5), hdg):WaypointAirTurningPoint(nil, SpeedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") + wp[2]=p1:Translate( UTILS.NMToMeters(10), hdg):WaypointAirTurningPoint(nil, SpeedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") else -- TODO: Test and tune! wp[2]=p1:WaypointAirTurningPoint(nil, SpeedTransit, {TaskArrivedHolding}, "Entering Marshal Pattern") end end - - + -- Set up waypoints including collapsing the stack. for stack=nstack, 1, -1 do @@ -2794,7 +2795,8 @@ function AIRBOSS:_MarshalAI(flight, nstack) end -- Waypoint. - wp[#wp+1]=p0:WaypointAirTurningPoint(nil, SpeedTransit, {TaskOrbit}, text) + -- TODO: p0? + wp[#wp+1]=p1:WaypointAirTurningPoint(nil, SpeedTransit, {TaskOrbit}, text) end @@ -2817,14 +2819,15 @@ function AIRBOSS:_LandAI(flight) local Speed=UTILS.KnotsToKmph(272) local Carrier=self:GetCoordinate() + local hdg=self:GetHeading() -- Waypoints array. local wp={} wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, Speed, {}, "Current position") - -- Landing waypoint. - wp[#wp+1]=self:GetCoordinate():SetAltitude(250):WaypointAirLanding(Speed, self.airbase, nil, "Landing") + -- Landing waypoint 5 NM behind carrier at 250 ASL. + wp[#wp+1]=self:GetCoordinate():Translate(-UTILS.NMToMeters(5), hdg):SetAltitude(250):WaypointAirLanding(Speed, self.airbase, nil, "Landing") -- Reinit waypoints. flight.group:WayPointInitialize(wp) @@ -2864,13 +2867,13 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) angels0=2 -- Distance 2.5 NM. - Dist=UTILS.NMToMeters(2.5) + Dist=UTILS.NMToMeters(2.5*math.sqrt(2)) -- Get true heading of carrier. local hdg=self.carrier:GetHeading() -- Center of holding pattern point. We give it a little head start -70 instead of -90 degrees. - p1=Carrier:Translate(Dist, hdg-70) + p1=Carrier:Translate(Dist, hdg-45) else -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. angels0=6 @@ -3005,9 +3008,14 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) self:MessageToPlayer(mflight, text, "MARSHAL") end - -- Also decrease flag for section members of flight. + -- Debug info. + self:I(string.format("Flight %s case %d is changing marshal stack %d --> %d.", mflight.groupname, mflight.case, mstack, mstack-1)) + + -- Loop over section members. for _,_sec in pairs(mflight.section) do local sec=_sec --#AIRBOSS.PlayerData + + -- Also decrease flag for section members of flight. sec.flag:Set(mstack-1) -- Inform section member. @@ -3029,30 +3037,30 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- Debug self:I(self.lid..string.format("Flight %s is leaving stack but not going to pattern.", flight.groupname)) - -- New time stamp for time in pattern. - flight.time=timer.getAbsTime() - - -- Set flag to -1. + -- Set flag to -1. -1 is rather arbitrary. Should not be -100 or positive. flight.flag:Set(-1) - + else -- Debug - self:I(self.lid..string.format("Flight %s is commencing pattern.", flight.groupname)) - - -- New time stamp for time in pattern. - flight.time=timer.getAbsTime() + local Tmarshal=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + self:I(self.lid..string.format("Flight %s is leaving marshal after %s and going pattern.", flight.groupname, Tmarshal)) -- Decrease flag. flight.flag:Set(stack-1) -- Add flight to pattern queue. table.insert(self.Qpattern, flight) - - -- Remove flight from marshal queue. - self:_RemoveGroupFromQueue(self.Qmarshal, flight.group) - + end + + -- New time stamp for time in pattern. + flight.time=timer.getAbsTime() + + + -- Remove flight from marshal queue. + self:_RemoveGroupFromQueue(self.Qmarshal, flight.group) + end --- Get next free stack depending on recovery case. Note that here we assume one flight group per stack! @@ -3150,7 +3158,8 @@ function AIRBOSS:_PrintQueue(queue, name) local flight=_flight --#AIRBOSS.FlightGroup -- Timestamp. - local clock=UTILS.SecondsToClock(flight.time) + --local clock=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + local clock=timer.getAbsTime()-flight.time -- Recovery case of flight. local case=flight.case -- Stack and stack alt. @@ -3165,6 +3174,11 @@ function AIRBOSS:_PrintQueue(queue, name) local nsec=#flight.section local actype=flight.actype local onboard=flight.onboard + local holding="false" + if flight.holding then + holding="true" + end + -- TODO: Include player data. --[[ if not flight.ai then @@ -3182,8 +3196,11 @@ function AIRBOSS:_PrintQueue(queue, name) k=playerData.waveoff end ]] - text=text..string.format("\n[%d] %s*%d (%s): lead=%s (%d), onboard=%s, stackalt=%d ft, flag=%d, case=%d, time=%s, fuel=%d, ai=%s", - i, flight.groupname, flight.nunits, actype, lead, nsec, onboard, alt, stack, case, clock, fuel, ai) + text=text..string.format("\n[%d] %s*%d (%s): lead=%s (%d), onboard=%s, flag=%d, case=%d, time=%d, fuel=%d, ai=%s, holding=%s", + i, flight.groupname, flight.nunits, actype, lead, nsec, onboard, stack, case, clock, fuel, ai, holding) + if flight.holding then + text=text..string.format(" stackalt=%d ft", alt) + end end end self:I(self.lid..text) @@ -3226,6 +3243,7 @@ function AIRBOSS:_CreateFlightGroup(group) flight.seclead=flight.group:GetUnit(1):GetName() -- Sec lead is first unitname of group but player name for players. flight.section={} flight.ballcall=false + flight.holding=nil -- Note, this should be re-set elsewhere! flight.case=self.case @@ -7642,6 +7660,9 @@ function AIRBOSS:_SetSection(_unitName) text=string.format("You are already in the Pattern queue. Setting section no possible any more!") else + -- Init array + playerData.section={} + -- Loop over all registered flights. for _,_flight in pairs(self.flights) do local flight=_flight --#AIRBOSS.FlightGroup diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 8d3ec8236..88af72b1e 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -215,7 +215,7 @@ RECOVERYTANKER = { --- Class version. -- @field #string version -RECOVERYTANKER.version="0.9.7" +RECOVERYTANKER.version="0.9.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -842,9 +842,9 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) -- Waypoints array. local wp={} - -- New waypoint with orbit pattern task. - wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil , self.speed, {}, "Current Position") - wp[2]=p0:WaypointAirTurningPoint(nil, self.speed, {taskorbit}, "Tanker Orbit") + -- New waypoint with orbit pattern task. Speed expected in km/h. + wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil , UTILS.MpsToKmph(self.speed), {}, "Current Position") + wp[2]=p0:WaypointAirTurningPoint(nil, UTILS.MpsToKmph(self.speed), {taskorbit}, "Tanker Orbit") --local wp=self:_Pattern() @@ -900,7 +900,7 @@ function RECOVERYTANKER:_Pattern() local coord=p[i] --Core.Point#COORDINATE coord:MarkToAll(string.format("Waypoint %d", i)) --table.insert(wp, coord:WaypointAirFlyOverPoint(nil , self.speed)) - table.insert(wp, coord:WaypointAirTurningPoint(nil , self.speed)) + table.insert(wp, coord:WaypointAirTurningPoint(nil , UTILS.MpsToKmph(self.speed))) end return wp @@ -1098,11 +1098,11 @@ function RECOVERYTANKER:_InitRoute(dist, delay) -- Waypoints. local wp={} if self.takeoff==SPAWN.Takeoff.Air then - wp[#wp+1]=self.tanker:GetCoordinate():SetAltitude(self.altitude):WaypointAirTurningPoint(nil, self.speed, {}, "Spawn Position") + wp[#wp+1]=self.tanker:GetCoordinate():SetAltitude(self.altitude):WaypointAirTurningPoint(nil, UTILS.MpsToKmph(self.speed), {}, "Spawn Position") else wp[#wp+1]=Carrier:WaypointAirTakeOffParking() end - wp[#wp+1]=p:WaypointAirTurningPoint(nil, self.speed, {task}, "Begin Pattern") + wp[#wp+1]=p:WaypointAirTurningPoint(nil, UTILS.MpsToKmph(self.speed), {task}, "Begin Pattern") -- Set route. self.tanker:Route(wp, delay) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 6539918ac..59fca2782 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -558,7 +558,7 @@ function UTILS.SecondsToClock(seconds) -- Seconds of this day. local _seconds=seconds%(60*60*24) - if seconds <= 0 then + if seconds<0 then return nil else local hours = string.format("%02.f", math.floor(_seconds/3600)) From 7cf10e90f86731a1d5087d27092923bda9ba4fbd Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 14 Dec 2018 16:44:00 +0100 Subject: [PATCH 101/147] AIRBOSS 0.5.4w --- Moose Development/Moose/Ops/Airboss.lua | 315 +++++++++++++----- .../Moose/Ops/RecoveryTanker.lua | 123 +++---- Moose Development/Moose/Ops/RescueHelo.lua | 146 ++++++-- 3 files changed, 417 insertions(+), 167 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 9bc4befd6..ef2ecaaef 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -2,7 +2,7 @@ -- -- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers. -- --- Main features: +-- **Main Features:** -- -- * CASE I, II and III recoveries. -- * Supports human pilots as well as AI flight groups. @@ -12,33 +12,35 @@ -- * Automatic TACAN and ICLS channel setting of carrier. -- * Separate radio channels for LSO and Marshal transmissions. -- * Voice over support for LSO and Marshal radio transmissions. --- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, help function (player aircraft attitude, marking of pattern zones etc). +-- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, +-- help function (player aircraft attitude, marking of pattern zones etc). -- * Recovery tanker and refueling option via integration of @{#Ops.RecoveryTanker} class. -- * Rescue helo option via @{#Ops.RescueHelo} class. --- * Highly customizable by user API functions. +-- * Many parameters customizable by convenient user API functions. -- * Multiple carrier support due to object oriented approach. --- * Finite State Machine (FSM) implementation. --- --- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much **work in progress**. --- Your constructive feedback is both necessary and highly appreciated. +-- * Finite State Machine (FSM) implementat -- -- Supported Carriers: -- --- * USS John C. Stennis: +-- * USS John C. Stennis -- -- Supported Player and AI Aircraft: -- -- * F/A-18C Hornet Lot 20 (player+AI) --- * A-4E-C community mod (player+AI) --- * F/A-18C (AI) --- * F-14A (AI) --- * E-2D (AI) --- * S-3B (AI) +-- * A-4E-C Skyhawk Community Mod (player+AI) +-- * F/A-18C Hornet (AI) +-- * F-14A Tomcat (AI) +-- * E-2D Hawkeye (AI) +-- * S-3B Viking & tanker version (AI) -- -- At the moment, parameters are optimized for F/A-18C Hornet as aircraft and USS John C. Stennis as carrier. -- The community A-4E mod is also supported in priciple but maybe needs further tweaking of parameters such as on speed AoA values. -- --- Other aircraft and carriers **might** be possible in future but would need a different set of optimized parameters. +-- Other aircraft and carriers *might* be possible in future but would need a different set of optimized individual parameters. +-- *Winter is coming!* +-- +-- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much **work in progress**. +-- Your constructive feedback is both necessary and highly appreciated. -- -- === -- @@ -126,32 +128,66 @@ -- -- The AIRBOSS class supports all three commonly used recovery cases, i.e. -- --- * CASE I: For daytime and good weather, --- * CASE II: For daytime but poor visibility conditions, --- * CASE III: For nighttime recoveries. +-- * **CASE I** during daytime and good weather, +-- * **CASE II** during daytime with poor visibility conditions, +-- * **CASE III** during nighttime recoveries. -- -- That being said, this script allows you to use any of the three cases to be used at any time. Or, in other words, *you* need to specify when which case is safe and appropriate. -- -- This is a lot of responsability. *You* are the boss, but *you* need to make the right decisions or things will go terribly wrong! +-- +-- Recovery windows can be set up via the @{#AIRBOSS.AddRecoveryWindow} function as explained below. With this it is possible to seamlessly switch recovery cases even in the same mission. -- -- ## CASE I -- --- ### Holding Pattern +-- As mentioned before, Case I recovery is the standard procedure during daytime and good visibility conditions. -- +-- ### Holding Pattern +-- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Holding.png) -- +-- The graphic depicts a the standard holding pattern during a Case I recovery. Incoming aircraft enter the holding pattern, which is a counter clockwise turn with a +-- diameter of 5 NM, at their assigned altiude. The holding altitude of the first stack is 2000 ft. The inverval between stacks is 1000 ft. +-- +-- Once a recovery window opens, the aircraft of the lowest stack commence their landing approach and the rest of the Marshal stack collapses, i.e. aircraft switch from +-- their current stack to the next lower stack. +-- +-- The flight that transitions form the holding pattern to the landing approach, it should leave the Marshal stack at the 3 position and make a left hand turn to the *Initial* +-- position, which is 3 NM astern of the boat. +-- -- ### Landing Pattern -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Landing.png) -- +-- Once the aircraft reaches the Inital, the landing pattern begins. The important steps of the pattern are shown in the image above. +-- -- ## CASE III -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3.png) -- +-- A Case III recovery is conducted during nighttime. The holding positon and the landing pattern are very different from a Case I recovery as can be seen in the image above. +-- +-- The first holding zone starts 21 NM astern the carrier at angels 6. The interval between the stacks is 1000 ft just like in Case I. However, the distance to the boat also +-- increases by 1 NM with each stack. The general form can be written as D=15+6+(N-1), where D is the distance to the boat in NM and N the number of the stack starting at one. +-- +-- Once the aircraft of the lowest stack is allowed to commence to the landing pattern, it starts a descent at 4000 ft/min until it reaches the "*Platform*" at 5000 ft and +-- ~19 NM DME. From there a shallower descent at 2000 ft/min should be performed. At an altitude of 1200 ft the aircraft should level out and "*Dirty Up*" (gear & hook down). +-- +-- At 3 NM distance to the carrier, the aircraft should intercept the 3.5 degrees glide slope at the "*Bullseye*". From there the pilot should "follow the needes" of the ICLS. +-- -- ## CASE II -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case2.png) -- +-- Case II is the common recovery procedure at daytime if visibilty conditions are poor. It can be viewed as hybrid between Case I and III. +-- The holding pattern is very similar to that of the Case III recovery with the difference the the radial is the inverse of the BRC instead of the FB. +-- From the holding zone aircraft are follow the Case III path until they reach the Initial position 3 NM astern the boat. From there a standard Case I recovery procdure is +-- in place. +-- +-- Note that the image depicts the case, where the holding zone has an angle offset off 30 degrees with respect to the BRC. This is optional. Commonly used offset angles +-- are 0 (no offset), +-15 degrees or +-30 degrees. The AIRBOSS class supports all these scenarios which are used during Case II and III recoveries. +-- +-- -- # Scripting -- -- Writing a basic script is easy and can be done in two lines. @@ -324,17 +360,69 @@ -- * **IM** In the Middle -- * **IC** In Close -- * **AR** At the Ramp --- * **IW** In the Wires +-- * **IW** In the Wiress -- -- Grading at each step includes the above calls, i.e. -- --- * Linup: (LUL), LUL, _LUL_, (RUL), RUL, _RUL_ --- * Alitude: (H), H, _H_, (L), L, _L_ --- * Speed: (F), F, _F_, (S), S, _S_ +-- * Linup: (LUL), LUL, _LUL_, (RUL), RUL, \_RUL\_ +-- * Alitude: (H), H, _H_, (L), L, \_L\_ +-- * Speed: (F), F, _F_, (SLO), SLO, \_SLO\_ -- --- The position at the landing even is analyses and the corresponding trapped wire calculated. If no wire was caught, the LSO will give the bolter call. +-- The position at the landing event is analyzed and the corresponding trapped wire calculated. If no wire was caught, the LSO will give the bolter call. -- --- If a player is sigifiantly off from the ideal parameters in close or at the ramp, the LSO will wave off the player. +-- If a player is sigifiantly off from the ideal parameters in close or at the ramp, the LSO will wave the player off. +-- +-- ## Pattern Wave Off +-- +-- The player's aircraft position is evaluated at certain critical locations in the landing pattern. If the player is far off from the ideal approach, the LSO will +-- issue a pattern wave off. Currently, this is only implemented for Case I recoveries and the Case I part in the Case II recovery, i.e. +-- +-- * Break Entry +-- * Early Break +-- * Late Break +-- * Abeam +-- * Ninety +-- * Wake +-- * Groove +-- +-- At these points it is also checked if a player comes too close to another aircraft ahead of him in the pattern. +-- +-- # AI Handling +-- +-- The implementation allows to handle incoming AI units and integrate them into the marshal and landing pattern. +-- +-- By default, incoming carrier capable aircraft which are detecting inside the CCZ and approach the carrier by more than 5 NM are automatically guided to the holding zone. +-- Each AI group gets its own marshal stack in the holding pattern. Once a recovery window opens, the AI group of the lowest stack is transitioning to the landing pattern +-- and the Marshal stack collapses. +-- +-- If no AI handling is desired, this can be turned off via the @{#AIRBOSS.SetHandleAIOFF} function. +-- +-- ## Known Issues +-- +-- The holding position of the AI is updated regularly when the carrier has changed its position by more then 2.5 NM or changed its course significantly. +-- The patterns are realized by orbit or racetrack patterns of the DCS scripting API. +-- However, when the position is updated or the marshal stack collapses, it comes to disruptions of the regular orbit becase a new waypoint with a new +-- orbit task needs to be created. +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#AIRBOSS} class should have the string "AIRBOSS" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("AIRBOSS") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ## Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#AIRBOSS.SetDebugModeON} function. +-- If enabled, status and debug text messages will be displayed on the screen. Also informative marks on the F10 map are created. -- -- @field #AIRBOSS AIRBOSS = { @@ -418,7 +506,7 @@ AIRBOSS.AircraftPlayer={ -- @field #string S3BTANKER Lockheed S-3B Viking tanker. -- @field #string E2D Grumman E-2D Hawkeye AWACS. -- @field #string FA18C F/A-18C Hornet (AI). --- @field #string F14A F-14A (AI). +-- @field #string F14A F-14A Tomcat (AI). AIRBOSS.AircraftCarrier={ --AV8B="AV8BNA", HORNET="FA-18C_hornet", @@ -929,7 +1017,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.4" +AIRBOSS.version="0.5.4w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1456,6 +1544,22 @@ function AIRBOSS:SetWarehouse(warehouse) return self end +--- Activate debug mode. Display debug messages on screen. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetDebugModeON() + self.Debug=true + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetDebugModeOFF() + self.Debug=false + return self +end + --- Check if carrier is recovering aircraft. -- @param #AIRBOSS self -- @return #boolean If true, time slot for recovery is open. @@ -1626,15 +1730,14 @@ function AIRBOSS:_CheckAIStatus() -- Check if parameters are right and flight is in the groove. if lineup<2 and distance<=0.75 and alt<500 and not element.ballcall then - -- Paddles: Call the ball! + -- Paddles: Call the ball! self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.CALLTHEBALL, false, 0) -- Pilot: "405, Hornet Ball, 3.2" - -- TODO: Message to players only. -- TODO: Voice over. - local text=string.format("%s, %s Ball, %.1f.", element.onboard, self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) - MESSAGE:New(text, 15):ToCoalition(self:GetCoalition()) - --self:MessageToPlayer(playerData, text, playerData.onboard, "", 3, false, 3) + local text=string.format("%s Ball, %.1f.", self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) + self:MessageToPattern(text, element.onboard, "", 3, false, 0, true) + MESSAGE:New(text, 15):ToAll() -- Paddles: Roger ball after 3 seconds. self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 3) @@ -1677,7 +1780,10 @@ function AIRBOSS:_CheckPlayerPatternDistance(player) return false end - -- TODO: return false when unit2 is not in air? Could be on the carrier. + -- Return false when unit2 is not in air? Could be on the carrier. + if not unit2:InAir() then + return false + end -- Positions of units. local c1=unit1:GetCoordinate() @@ -1696,11 +1802,12 @@ function AIRBOSS:_CheckPlayerPatternDistance(player) -- Get angle between the two orientation vectors. Does the player aircraft nose point into the direction of the other aircraft? (Could be behind him!) local rhdg=math.deg(math.acos(UTILS.VecDot(vec12,vec1)/UTILS.VecNorm(vec12)/UTILS.VecNorm(vec1))) - -- TODO: Check altitude difference? + -- Check altitude difference? + local dalt=math.abs(c2.y-c1.y) - -- Direction in 30 degrees cone and distance < 200 meters. + -- Direction in 30 degrees cone and distance < 200 meters and altitude difference <50 -- TODO: Test parameter values. - if math.abs(rhdg)<30 and dist<200 then + if math.abs(rhdg)<30 and dist<200 and dalt<50 then return true else return false @@ -1939,17 +2046,26 @@ function AIRBOSS._PassingWaypoint(group, airboss, i, final) -- Debug message. local text=string.format("Group %s passing waypoint %d of %d.", group:GetName(), i, final) + -- Debug smoke and marker. if airboss.Debug then local pos=group:GetCoordinate() pos:SmokeRed() local MarkerID=pos:MarkToAll(string.format("Group %s reached waypoint %d", group:GetName(), i)) end + -- Debug message. MESSAGE:New(text,10):ToAllIf(airboss.Debug) airboss:T(airboss.lid..text) -- Set current waypoint. airboss.currentwp=i + + -- If final waypoint reached, do route all over again. + if i==final then + -- TODO: set task to call this routine again when carrier reaches final waypoint if user chooses to. + -- SetPatrolAdInfinitum user function + airboss:_PatrolRoute() + end end --- Function called when a group has reached the holding zone. @@ -1992,8 +2108,7 @@ function AIRBOSS:_PatrolRoute() -- NOTE: This is only necessary, if the first waypoint would already be far way, i.e. when the script is started with a large delay. -- Calculate the new Route. - --local wp0=CarrierGroup:GetCoordinate():WaypointGround(5.5*3.6) - + --local wp0=CarrierGroup:GetCoordinate():WaypointGround(5.5*3.6) -- Insert current coordinate as first waypoint --table.insert(Waypoints, 1, wp0) @@ -2005,9 +2120,6 @@ function AIRBOSS:_PatrolRoute() -- Call task function when carrier arrives at waypoint. CarrierGroup:SetTaskWaypoint(Waypoints[n], TaskPassingWP) end - - -- TODO: set task to call this routine again when carrier reaches final waypoint if user chooses to. - -- SetPatrolAdInfinitum user function -- Set waypoint table. local i=1 @@ -7129,7 +7241,6 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration else -- Send onboard number so that player is alerted about the text message. - -- DONE: This will fail with message to all since for each player the message will be played! if receiver==playerData.onboard and not soundoff then if sender then if sender=="LSO" then @@ -7163,36 +7274,88 @@ end -- @param #boolean soundoff If true, do not play boad number message. function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay, soundoff) - local playit=true -- In case two have the same flight number. + -- Make sure the onboard number sound is played only once. + local soundoff=false + for _,_player in pairs(self.players) do local playerData=_player --#AIRBOSS.PlayerData -- Message to all players in CCA. - -- TODO: could make something to all in pattern or all in marshal queue depending on sender. if playerData.unit:IsInZone(self.zoneCCA) then - - -- Play receiver board number. Best we can do if no voice over for the whole message is there. - if receiver==playerData.onboard and sender and playit and not soundoff then - -- Check who is the sender. - if sender=="LSO" then - -- Sender is LSO or AIRBOSS ==> Broadcast on LSO radio. - self:_Number2Sound(self.LSORadio, receiver, delay) - elseif sender=="MARSHAL" then - -- Sender is MARSHAL ==> Broadcast on MARSHAL radio. - self:_Number2Sound(self.MarshalRadio, receiver, delay) - end - playit=false -- Play only once, in case two have the same flight number. - end -- Message to player. - self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, true) + self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) + -- Disable sound play of onboard number. + soundoff=true end - end - end + +--- Send text message to all players in the pattern queue. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +-- @param #boolean soundoff If true, do not play boad number message. +function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, delay, soundoff) + + -- Make sure the onboard number sound is played only once. + local soundoff=false + + -- Loop over all flights in the pattern queue. + for _,_player in pairs(self.Qpattern) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Message only to human pilots. + if not playerData.ai then + + -- Message to player. + self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) + + -- Disable sound play of onboard number. + soundoff=true + end + end +end + +--- Send text message to all players in the marshal queue. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +-- @param #boolean soundoff If true, do not play boad number message. +function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, delay, soundoff) + + -- Make sure the onboard number sound is played only once. + local soundoff=false + + -- Loop over all flights in the marshal queue. + for _,_player in pairs(self.Qmarshal) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Message only to human pilots. + if not playerData.ai then + + -- Message to player. + self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) + + -- Disable sound play of onboard number. + soundoff=true + end + end +end + + --- Convert a number (as string) into a radio message. -- E.g. for board number or headings. -- @param #AIRBOSS self @@ -7279,8 +7442,11 @@ function AIRBOSS:_AddF10Commands(_unitName) -- Get group and ID. local group=_unit:GetGroup() local gid=group:GetID() + + -- Player Data. + local playerData=self.players[playername] - if group and gid then + if group and gid and playerData then if not self.menuadded[gid] then @@ -7292,9 +7458,6 @@ function AIRBOSS:_AddF10Commands(_unitName) AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss") end - -- Player Data. - local playerData=self.players[playername] - -- F10/Airboss/ local _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) @@ -7311,19 +7474,19 @@ function AIRBOSS:_AddF10Commands(_unitName) -- F10/Airboss//F1 Help/F2 Mark Zones local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) -- F10/Airboss//F1 Help/F3 Mark Zones/ - missionCommands.addCommandForGroup(gid, "Smoke My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F1 - missionCommands.addCommandForGroup(gid, "Flare My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F2 - missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F3 - missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F4 + missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 + missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 + missionCommands.addCommandForGroup(gid, "Smoke My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F3 + missionCommands.addCommandForGroup(gid, "Flare My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F4 -- F10/Airboss//F1 Help/ - missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F4 - missionCommands.addCommandForGroup(gid, "Attitude Monitor ON/OFF", _helpPath, self._AttitudeMonitor, self, playername) -- F5 - missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) -- F6 - missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) -- F7 - missionCommands.addCommandForGroup(gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) -- F8 + missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F4 + missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._AttitudeMonitor, self, playername) -- F5 + missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) -- F6 + missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) -- F7 + missionCommands.addCommandForGroup(gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) -- F8 ------------------------------------- - -- F10/Airboss//F2 Kneeboard -- + -- F10/Airboss//F2 Kneeboard ------------------------------------- local _kneeboardPath=missionCommands.addSubMenuForGroup(gid, "Kneeboard", _rootPath) -- F10/Airboss//F2 Kneeboard/F1 Results @@ -7337,9 +7500,9 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(gid, "Weather Report", _kneeboardPath, self._DisplayCarrierWeather, self, _unitName) -- F3 missionCommands.addCommandForGroup(gid, "Set Section", _kneeboardPath, self._SetSection, self, _unitName) -- F4 - ---------------------------- - -- F10/Airboss// -- - ---------------------------- + ------------------------- + -- F10/Airboss// + ------------------------- missionCommands.addCommandForGroup(gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) -- F3 missionCommands.addCommandForGroup(gid, "Request Commence", _rootPath, self._RequestCommence, self, _unitName) -- F4 missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) -- F5 diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 88af72b1e..4896ec6b1 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -2,15 +2,15 @@ -- -- Tanker aircraft flying a racetrack pattern overhead an aircraft carrier. -- --- Features: +-- **Main Features:** -- -- * Regular pattern update with respect to carrier positon. -- * Automatic respawning when tanker runs out of fuel for 24/7 operations. -- * Tanker can be spawned cold or hot on the carrier or at any other airbase or directly in air. -- * Automatic AA TACAN beacon setting. +-- * Multiple tanker at different carriers due to object oriented approach. -- * Finite State Machine (FSM) implementation, which allows the mission designer to hook into certain events. -- --- -- === -- -- ### Author: **funkyfranky** @@ -23,7 +23,8 @@ -- @type RECOVERYTANKER -- @field #string ClassName Name of the class. -- @field #boolean Debug Debug mode. --- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. +-- @field #string lid Log debug id text. +-- @field Wrapper.Unit#UNIT carrier The carrier the tanker is attached to. -- @field #string carriertype Carrier type. -- @field #string tankergroupname Name of the late activated tanker template group. -- @field Wrapper.Group#GROUP tanker Tanker group. @@ -60,7 +61,7 @@ -- -- # Recovery Tanker -- --- A recovery tanker acts as refueling unit flying overhead an aircraft carrier in order to supply incoming flights with gas if necessary. +-- A recovery tanker acts as refueling unit flying overhead an aircraft carrier in order to supply incoming flights with gas if they go "Bingo on the Ball". -- -- # Simple Script -- @@ -110,7 +111,7 @@ -- If only the first spawning should happen on the carrier, one use the @{#RECOVERYTANKER.SetRespawnInAir}() function to command that all subsequent spawning -- will happen in air. -- --- If the helo should not be respawned at all, one can set @{#RECOVERYTANKER.SetRespawnOff}(). +-- If the tanker should not be respawned at all, one can set @{#RECOVERYTANKER.SetRespawnOff}(). -- -- ## Pattern Parameters -- @@ -136,7 +137,6 @@ -- -- In order to completely disable the TACAN beacon, you can use the @{#RECOVERYTANKER.SetTACANoff}() function in your script. -- --- -- ## Pattern Update -- -- The pattern of the tanker is updated if at least one of the two following conditions apply: @@ -150,9 +150,9 @@ -- The maximum update frequency is set to 10 minutes. You can adjust this by @{#RECOVERYTANKER.SetPatternUpdateInterval}. -- Also the pattern will not be updated while the carrier is turning or the tanker is currently refuelling another unit. -- --- # Finite State Model +-- # Finite State Machine -- --- The implementation uses a Finite State Model (FSM). This allows the mission designer to hook in to certain events. +-- The implementation uses a Finite State Machine (FSM). This allows the mission designer to hook in to certain events. -- -- * @{#RECOVERYTANKER.Start}: This event starts the FMS process and initialized parameters and spawns the tanker. DCS event handling is started. -- * @{#RECOVERYTANKER.Status}: This event is called in regular intervals (~60 seconds) and checks the status of the tanker and carrier. It triggers other events if necessary. @@ -179,11 +179,17 @@ -- BASE:TraceClass("RECOVERYTANKER") -- -- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ## Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#RECOVERYTANKER.SetDebugModeON} function. +-- If enabled, text messages about the tanker status will be displayed on screen and marks of the pattern created on the F10 map. -- -- @field #RECOVERYTANKER RECOVERYTANKER = { ClassName = "RECOVERYTANKER", Debug = false, + lid = nil, carrier = nil, carriertype = nil, tankergroupname = nil, @@ -263,6 +269,9 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- Save self in static object. Easier to retrieve later. self.carrier:SetState(self.carrier, "RECOVERYTANKER", self) + -- Debug log id. + self.lid=string.format("RECOVERYTANKER %s", self.carrier:GetName()) + -- Init default parameters. self:SetAltitude() self:SetSpeed() @@ -359,7 +368,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. --- On after "RTB" event user function. Called when a the the tanker returns to its home base. - -- @function [parent=#RECOVERYTANKER] OnAfterPatternUpdate + -- @function [parent=#RECOVERYTANKER] OnAfterRTB -- @param #RECOVERYTANKER self -- @param #string From From state. -- @param #string Event Event. @@ -731,7 +740,7 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) -- Get fuel of tanker. local fuel=self.tanker:GetFuel()*100 local text=string.format("Recovery tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) - self:T(text) + self:T(self.lid..text) -- Check if tanker flies through pattern update zone. -- TODO: Check if this can be used to update the pattern without too much disruption. @@ -760,7 +769,8 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) -- Debug message. local text=string.format("Respawning recovery tanker %s in air.", self.tanker:GetName()) - self:T(text) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) -- Respawn tanker. self.tanker:InitHeading(self.tanker:GetHeading()) @@ -816,7 +826,9 @@ end function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) -- Debug message. - self:T(string.format("Updating recovery tanker %s racetrack pattern.", self.tanker:GetName())) + local text=string.format("Updating recovery tanker %s racetrack pattern.", self.tanker:GetName()) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) -- Carrier heading. local hdg=self.carrier:GetHeading() @@ -865,12 +877,11 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) end ---- Self made race track pattern. +--- Self made race track pattern. (not used) -- @param #RECOVERYTANKER self -- @return #table Table of pattern waypoints. function RECOVERYTANKER:_Pattern() - -- Carrier heading. local hdg=self.carrier:GetHeading() @@ -919,7 +930,8 @@ function RECOVERYTANKER:onafterRTB(From, Event, To, airbase) -- Debug message. local text=string.format("Recoery tanker %s returning to airbase %s.", self.tanker:GetName(), airbase:GetName()) - self:T(text) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) -- Waypoint array. local wp={} @@ -968,7 +980,10 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) if groupname:match(self.tankergroupname) then -- Debug info. - self:T(string.format("Respawning recovery tanker group %s.", group:GetName())) + local text=string.format("Respawning recovery tanker group %s.", group:GetName()) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + -- Respawn tanker. self.tanker=group:RespawnAtCurrentAirbase() @@ -1004,7 +1019,9 @@ function RECOVERYTANKER:_RefuelingStart(EventData) end -- Info message. - self:T(string.format("Recovery tanker %s started refueling unit %s", self.tanker:GetName(), receiver:GetName())) + local text=string.format("Recovery tanker %s started refueling unit %s", self.tanker:GetName(), receiver:GetName()) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) -- FMS state "Refueling". self:RefuelStart(receiver) @@ -1032,8 +1049,10 @@ function RECOVERYTANKER:_RefuelingStop(EventData) end -- Info message. - self:T(string.format("Recovery tanker %s stopped refueling unit %s", self.tanker:GetName(), receiver:GetName())) - + local text=string.format("Recovery tanker %s stopped refueling unit %s", self.tanker:GetName(), receiver:GetName()) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + -- FSM state "Running". self:RefuelStop(receiver) end @@ -1076,7 +1095,7 @@ function RECOVERYTANKER:_InitRoute(dist, delay) delay=delay or 1 -- Debug message. - self:T(string.format("Initializing route for recovery tanker %s.", self.tanker:GetName())) + self:T(self.lid..string.format("Initializing route of recovery tanker %s.", self.tanker:GetName())) -- Carrier position. local Carrier=self.carrier:GetCoordinate() @@ -1149,13 +1168,13 @@ function RECOVERYTANKER:_CheckPatternUpdate(dt) -- Debug output if turning if turning then - self:T2(string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) + self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) end -- Check if orientation changed. local Hchange=false if math.abs(deltaHeading)>=self.Hupdate then - self:T(string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) + self:T(self.lid..string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) Hchange=true end @@ -1165,7 +1184,7 @@ function RECOVERYTANKER:_CheckPatternUpdate(dt) -- Check if carrier moved more than ~10 km. local Dchange=false if dist>self.Dupdate then - self:T(string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) + self:T(self.lid..string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) Dchange=true end @@ -1177,6 +1196,12 @@ function RECOVERYTANKER:_CheckPatternUpdate(dt) -- Update if heading or distance changed. if Hchange or Dchange then + -- Debug message. + local text=string.format("Updating tanker %s pattern due to carrier change.", self.tanker:GetName()) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Update pos and orientation. self.orientation=vNew self.position=pos update=true @@ -1206,7 +1231,9 @@ function RECOVERYTANKER:_ActivateTACAN(delay) if unit:IsAlive() then -- Debug message. - self:T(string.format("Activating recovery tanker TACAN beacon: channel=%d mode=%s, morse=%s.", self.TACANchannel, self.TACANmode, self.TACANmorse)) + local text=string.format("Activating recovery tanker TACAN beacon: channel=%d mode=%s, morse=%s.", self.TACANchannel, self.TACANmode, self.TACANmorse) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) -- Create a new beacon and activate TACAN. self.beacon=BEACON:New(unit) @@ -1220,52 +1247,6 @@ function RECOVERYTANKER:_ActivateTACAN(delay) end ---- Calculate distances between carrier and tanker. --- @param #RECOVERYTANKER self --- @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 RECOVERYTANKER:_GetDistances() - - -- Vector to carrier - local a=self.carrier:GetVec3() - - -- Vector to player - local b=self.tanker: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 - - if phi<0 then - phi=phi+360 - end - - return dx,dz,rho,phi -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 15e58b986..71d67d596 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -2,12 +2,14 @@ -- -- Recue helicopter for carrier operations. -- --- Features: +-- **Main Features:** -- -- * Close formation with carrier. -- * Carrier can have any number of waypoints. -- * Automatic respawning on empty fuel for 24/7 operations. -- * Automatic rescuing of crashed or ejected units in the vicinity. +-- * Multiple helos at different carriers due to object oriented approach. +-- * Finite State Machine (FSM) implementation. -- -- === -- @@ -20,6 +22,7 @@ -- @type RESCUEHELO -- @field #string ClassName Name of the class. -- @field #boolean Debug Debug mode on/off. +-- @field #string lid Log debug id text. -- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. -- @field #string carriertype Carrier type. -- @field #string helogroupname Name of the late activated helo template group. @@ -53,7 +56,7 @@ -- # Recue Helo -- -- The rescue helo will fly in close formation with another unit, which is typically an aircraft carrier. --- It's mission is to rescue crashed units or ejected pilots. Well, and to look cool... +-- It's mission is to rescue crashed or ejected pilots. Well, and to look cool... -- -- # Simple Script -- @@ -120,7 +123,6 @@ -- -- Once the helo runs out of fuel, it will return to the USS Normandy and not the Stennis for respawning. -- --- -- ## Formation Positon -- -- The position of the helo relative to the mother ship can be tuned via the functions @@ -129,11 +131,44 @@ -- * @{#RESCUEHELO.SetOffsetX}(*distance*)}, where *distance is the distance in the direction of movement of the carrier. Default is 200 meters. -- * @{#RESCUEHELO.SetOffsetZ}(*distance*)}, where *distance is the distance on the starboard side. Default is 200 meters. -- +-- # Finite State Machine +-- +-- The implementation uses a Finite State Machine (FSM). This allows the mission designer to hook in to certain events. +-- +-- * @{#RESCUEHELO.Start}: This eventfunction starts the FMS process and initialized parameters and spawns the helo. DCS event handling is started. +-- * @{#RESCUEHELO.Status}: This eventfunction is called in regular intervals (~60 seconds) and checks the status of the helo and carrier. It triggers other events if necessary. +-- * @{#RESCUEHELO.Rescue}: This eventfunction commands the helo to go on a rescue operation at a certain coordinate. +-- * @{#RESCUEHELO.RTB}: This eventsfunction sends the helo to its home base (usually the carrier). This is called once the helo runs low on gas. +-- * @{#RESCUEHELO.Run}: This eventfunction is called when the helo resumes normal operations and goes back on station. +-- * @{#RESCUEHELO.Stop}: This eventfunction stops the FSM by unhandling DCS events. +-- +-- The mission designer can capture these events by RESCUEHELO.OnAfter*Eventname* functions, e.g. @{#RESCUEHELO.OnAfterRescue}. +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#RESCUEHELO} class should have the string "RESCUEHELO" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("RESCUEHELO") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ## Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#RESCUEHELO.SetDebugModeON} function. +-- If enabled, text messages about the helo status will be displayed on screen and marks of the pattern created on the F10 map. -- -- @field #RESCUEHELO RESCUEHELO = { ClassName = "RESCUEHELO", Debug = false, + lid = nil, carrier = nil, carriertype = nil, helogroupname = nil, @@ -160,14 +195,14 @@ RESCUEHELO = { --- Class version. -- @field #string version -RESCUEHELO.version="0.9.5" +RESCUEHELO.version="0.9.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Write documenation. -- TODO: Add option to stop carrier while rescue operation is in progress? Done but NOT working! +-- DONE: Write documenation. -- DONE: Add option to deactivate the rescueing. -- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. -- DONE: Add rescue event when aircraft crashes. @@ -199,6 +234,9 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- Helo group name. self.helogroupname=helogroupname + + -- Log ID. + self.lid=string.format("RESCUEHELO %s |", self.carrier:GetName()) -- Init defaults. self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) @@ -239,6 +277,7 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- @param #RESCUEHELO self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate. -- @function [parent=#RESCUEHELO] Rescue -- @param #RESCUEHELO self @@ -250,6 +289,15 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- @param #number delay Delay in seconds. -- @param Core.Point#COORDINATE RescueCoord Coordinate where the resue mission takes place. + --- On after "Rescue" event user function. Called when a the the helo goes on a rescue mission. + -- @function [parent=#RESCUEHELO] OnAfterRescue + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE RescueCoord Crash site where the rescue operation takes place. + + --- Triggers the FSM event "RTB" that sends the helo home. -- @function [parent=#RESCUEHELO] RTB -- @param #RESCUEHELO self @@ -259,6 +307,14 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- @param #RESCUEHELO self -- @param #number delay Delay in seconds. + --- On after "RTB" event user function. Called when a the the helo returns to its home base. + -- @function [parent=#RESCUEHELO] OnAfterRTB + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "Run". -- @function [parent=#RESCUEHELO] Run -- @param #RESCUEHELO self @@ -268,6 +324,17 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- @param #RESCUEHELO self -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status" that updates the helo status. + -- @function [parent=#RESCUEHELO] Status + -- @param #RESCUEHELO self + + --- Triggers the delayed FSM event "Status" that updates the helo status. + -- @function [parent=#RESCUEHELO] __Status + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped. -- @function [parent=#RESCUEHELO] Stop -- @param #RESCUEHELO self @@ -472,6 +539,21 @@ function RESCUEHELO:SetUseUncontrolledAircraft() return self end +--- Activate debug mode. Display debug messages on screen. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetDebugModeON() + self.Debug=true + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetDebugModeOFF() + self.Debug=false + return self +end --- Check if helo is returning to base. -- @param #RESCUEHELO self @@ -510,7 +592,9 @@ function RESCUEHELO:OnEventLand(EventData) if groupname:match(self.helogroupname) then -- Respawn the Helo. - self:I(string.format("Respawning rescue helo group %s at home base.", groupname)) + local text=string.format("Respawning rescue helo group %s at home base.", groupname) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then @@ -548,7 +632,9 @@ function RESCUEHELO:_OnEventCrashOrEject(EventData) if EventData.IniGroupName~=self.helo:GetName() then -- Debug. - self:T(string.format("Unit %s crashed or ejected.", unitname)) + local text=string.format("Unit %s crashed or ejected.", unitname) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) -- Unit "alive" and in our rescue zone. if unit:IsAlive() and unit:IsInZone(self.rescuezone) then @@ -557,7 +643,9 @@ function RESCUEHELO:_OnEventCrashOrEject(EventData) local coord=unit:GetCoordinate() -- Debug mark on map. - coord:MarkToCoalition(string.format("Crash site of unit %s.", unitname), self.helo:GetCoalition()) + if self.Debug then + coord:MarkToCoalition(self.lid..string.format("Crash site of unit %s.", unitname), self.helo:GetCoalition()) + end -- Only rescue if helo is "running" and not, e.g., rescuing already. if self:IsRunning() and self.rescueon then @@ -568,7 +656,7 @@ function RESCUEHELO:_OnEventCrashOrEject(EventData) else - self:I(string.format("Rescue helo %s crashed!", unitname)) + self:E(self.lid..string.format("Rescue helo %s crashed!", unitname)) end @@ -588,7 +676,8 @@ end function RESCUEHELO:onafterStart(From, Event, To) -- Events are handled my MOOSE. - self:I(string.format("Starting Rescue Helo Formation v%s for carrier unit %s of type %s.", RESCUEHELO.version, self.carrier:GetName(), self.carriertype)) + local text=string.format("Starting Rescue Helo Formation v%s for carrier unit %s of type %s.", RESCUEHELO.version, self.carrier:GetName(), self.carriertype) + self:I(self.lid..text) -- Handle events. --self:HandleEvent(EVENTS.Birth) @@ -699,7 +788,8 @@ function RESCUEHELO:onafterStatus(From, Event, To) -- Report current fuel. local text=string.format("Rescue Helo %s: state=%s fuel=%.1f", self.helo:GetName(), self:GetState(), fuel) - self:T(text) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) -- If fuel < threshold ==> send helo to home base! if fuel Date: Sat, 15 Dec 2018 08:47:01 +0100 Subject: [PATCH 102/147] AIBOSS v0.5.5 --- Moose Development/Moose/Ops/Airboss.lua | 109 +++++++++++++++++------- 1 file changed, 76 insertions(+), 33 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index ef2ecaaef..abd09ca20 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -18,7 +18,7 @@ -- * Rescue helo option via @{#Ops.RescueHelo} class. -- * Many parameters customizable by convenient user API functions. -- * Multiple carrier support due to object oriented approach. --- * Finite State Machine (FSM) implementat +-- * Finite State Machine (FSM) implementation. -- -- Supported Carriers: -- @@ -33,11 +33,11 @@ -- * E-2D Hawkeye (AI) -- * S-3B Viking & tanker version (AI) -- --- At the moment, parameters are optimized for F/A-18C Hornet as aircraft and USS John C. Stennis as carrier. --- The community A-4E mod is also supported in priciple but maybe needs further tweaking of parameters such as on speed AoA values. +-- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) as aircraft and the USS John C. Stennis as carrier. +-- The community A-4E mod is also supported in priciple but may needs further tweaking of parameters such as on speed AoA values. -- --- Other aircraft and carriers *might* be possible in future but would need a different set of optimized individual parameters. --- *Winter is coming!* +-- The implemenation is kept very general. So other including other aircraft and carriers in future is possible. (*Winter is coming!*) +-- But each aircraft or carrier needs a different set of optimized individual parameters. -- -- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much **work in progress**. -- Your constructive feedback is both necessary and highly appreciated. @@ -47,7 +47,7 @@ -- ### Author: **funkyfranky** -- ### Special thanks to -- **Bankler** for his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script! --- This gave the inspiration for this class. Also this class uses some functionalities for determining the player positon in Case I recoveries he developed. +-- His work was the initial inspiration for this class. Also note that this class uses some routines for determining the player position in Case I recoveries developed by Bankler. -- -- @module Ops.Airboss -- @image MOOSE.JPG @@ -112,6 +112,7 @@ -- @field DCS#Vec3 Corientation Carrier orientation in space. -- @field DCS#Vec3 Corientlast Last known carrier orientation. -- @field Core.Point#COORDINATE Cposition Carrier position. +-- @field #string defaultskill Default player skill @{#AIRBOSS.Difficulty}. -- @extends Core.Fsm#FSM --- The boss! @@ -165,10 +166,10 @@ -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3.png) -- --- A Case III recovery is conducted during nighttime. The holding positon and the landing pattern are very different from a Case I recovery as can be seen in the image above. +-- A Case III recovery is conducted during nighttime. The holding positon and the landing pattern are rather different from a Case I recovery as can be seen in the image above. -- --- The first holding zone starts 21 NM astern the carrier at angels 6. The interval between the stacks is 1000 ft just like in Case I. However, the distance to the boat also --- increases by 1 NM with each stack. The general form can be written as D=15+6+(N-1), where D is the distance to the boat in NM and N the number of the stack starting at one. +-- The first holding zone starts 21 NM astern the carrier at angels 6. The interval between the stacks is 1000 ft just like in Case I. However, the distance to the boat +-- increases by 1 NM with each stack. The general form can be written as D=15+6+(N-1), where D is the distance to the boat in NM and N the number of the stack starting at N=1. -- -- Once the aircraft of the lowest stack is allowed to commence to the landing pattern, it starts a descent at 4000 ft/min until it reaches the "*Platform*" at 5000 ft and -- ~19 NM DME. From there a shallower descent at 2000 ft/min should be performed. At an altitude of 1200 ft the aircraft should level out and "*Dirty Up*" (gear & hook down). @@ -181,11 +182,11 @@ -- -- Case II is the common recovery procedure at daytime if visibilty conditions are poor. It can be viewed as hybrid between Case I and III. -- The holding pattern is very similar to that of the Case III recovery with the difference the the radial is the inverse of the BRC instead of the FB. --- From the holding zone aircraft are follow the Case III path until they reach the Initial position 3 NM astern the boat. From there a standard Case I recovery procdure is +-- From the holding zone aircraft are follow the Case III path until they reach the Initial position 3 NM astern the boat. From there a standard Case I recovery procedure is -- in place. -- --- Note that the image depicts the case, where the holding zone has an angle offset off 30 degrees with respect to the BRC. This is optional. Commonly used offset angles --- are 0 (no offset), +-15 degrees or +-30 degrees. The AIRBOSS class supports all these scenarios which are used during Case II and III recoveries. +-- Note that the image depicts the case, where the holding zone has an angle offset of 30 degrees with respect to the BRC. This is optional. Commonly used offset angles +-- are 0 (no offset), +-15 or +-30 degrees. The AIRBOSS class supports all these scenarios which are used during Case II and III recoveries. -- -- -- # Scripting @@ -427,7 +428,7 @@ -- @field #AIRBOSS AIRBOSS = { ClassName = "AIRBOSS", - Debug = false, + Debug = true, lid = nil, carrier = nil, carriertype = nil, @@ -484,6 +485,7 @@ AIRBOSS = { Corientation = nil, Corientlast = nil, Cposition = nil, + defaultskill = nil, } --- Player aircraft types capable of landing on carriers. @@ -1017,12 +1019,14 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.4w" +AIRBOSS.version="0.5.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: +-- TODO: Subtitles off options on player level. -- TODO: PWO during case 2/3. Also when too close to other player. -- TODO: Option to filter AI groups for recovery. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! @@ -1132,6 +1136,9 @@ function AIRBOSS:New(carriername, alias) -- Set holding offset to 0 degrees. This set self.defaultoffset and self.holdingoffset. self:SetHoldingOffsetAngle() + + -- Default player skill EASY. + self:SetDefaultPlayerSkill(AIRBOSS.Difficulty.EASY) -- CCA 50 NM radius zone around the carrier. self:SetCarrierControlledArea() @@ -1339,7 +1346,8 @@ function AIRBOSS:SetRecoveryCase(case) end --- Set holding pattern offset from final bearing for Case II/III recoveries. --- Usually, this is +-15 or +-30 degrees. +-- Usually, this is +-15 or +-30 degrees. You should not use and offet angle >= 90 degrees, because this will cause a devision by zero in some of the equations used to calculate the approach corridor. +-- So best stick to the defaults up to 30 degrees. -- @param #AIRBOSS self -- @param #number offset Offset angle in degrees. Default 0. -- @return #AIRBOSS self @@ -1544,6 +1552,30 @@ function AIRBOSS:SetWarehouse(warehouse) return self end +--- Set default player skill. New players will be initialized with this skill. +-- +-- * "Flight Student" = @{#AIRBOSS.Difficulty.Easy} +-- * "Naval Aviator" = @{#AIRBOSS.Difficulty.Normal} +-- * "TOPGUN Graduate" = @{#AIRBOSS.Difficulty.Hard} +-- @param #AIRBOSS self +-- @param #string skill Player skill. Default "Naval Aviator". +-- @return #ARIBOSS self +function AIRBOSS:SetDefaultPlayerSkill(skill) + self.defaultskill=skill or AIRBOSS.Difficulty.NORMAL + -- Check that defualt skill is valid. + local gotit=false + for _,_skill in pairs(AIRBOSS.Difficulty) do + if _skill==self.defaultskill then + gotit=true + end + end + if not gotit then + self.defaultskill=AIRBOSS.Difficulty.NORMAL + self:E(self.lid..string.format("ERROR: Invalid default skill = %s. Resetting to Naval Aviator.", tostring(skill))) + end + return self +end + --- Activate debug mode. Display debug messages on screen. -- @param #AIRBOSS self -- @return #AIRBOSS self @@ -2047,7 +2079,7 @@ function AIRBOSS._PassingWaypoint(group, airboss, i, final) local text=string.format("Group %s passing waypoint %d of %d.", group:GetName(), i, final) -- Debug smoke and marker. - if airboss.Debug then + if airboss.Debug and false then local pos=group:GetCoordinate() pos:SmokeRed() local MarkerID=pos:MarkToAll(string.format("Group %s reached waypoint %d", group:GetName(), i)) @@ -2061,7 +2093,7 @@ function AIRBOSS._PassingWaypoint(group, airboss, i, final) airboss.currentwp=i -- If final waypoint reached, do route all over again. - if i==final then + if i==final and final>1 then -- TODO: set task to call this routine again when carrier reaches final waypoint if user chooses to. -- SetPatrolAdInfinitum user function airboss:_PatrolRoute() @@ -2078,7 +2110,7 @@ function AIRBOSS._ReachedHoldingZone(group, airboss, flight) local text=string.format("Group %s has reached the holding zone.", group:GetName()) -- Debug mark. - if airboss.Debug then + if airboss.Debug and false then local pos=group:GetCoordinate() local MarkerID=pos:MarkToAll(string.format("Flight group %s reached holding zone.", group:GetName())) end @@ -2821,6 +2853,9 @@ function AIRBOSS:_MarshalAI(flight, nstack) -- Current carrier position. local Carrier=self:GetCoordinate() + + -- Carrier heading. + local hdg=self:GetHeading() -- Aircraft speed 272 knots when orbiting the pattern. (Orbit expects m/s.) local SpeedOrbit=UTILS.KnotsToMps(272) @@ -2852,15 +2887,12 @@ function AIRBOSS:_MarshalAI(flight, nstack) -- Task function when arriving at the holding zone. This will set flight.holding=true. local TaskArrivedHolding=flight.group:TaskFunction("AIRBOSS._ReachedHoldingZone", self, flight) - - -- Carrier heading. - local hdg=self:GetHeading() if flight.case==1 then -- Waypoint "north" of carrier's holding zone. --wp[2]=p1:Translate(UTILS.NMToMeters(10), hdg):WaypointAirTurningPoint(nil, SpeedTransit, {}, "Prepare Entering Case I Marshal Pattern") -- Enter pattern from "north" to "south". - wp[2]=p1:Translate( UTILS.NMToMeters(10), hdg):WaypointAirTurningPoint(nil, SpeedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") + wp[2]=Carrier:Translate(UTILS.NMToMeters(10), hdg-30):WaypointAirTurningPoint(nil, SpeedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") else -- TODO: Test and tune! wp[2]=p1:WaypointAirTurningPoint(nil, SpeedTransit, {TaskArrivedHolding}, "Entering Marshal Pattern") @@ -2882,7 +2914,10 @@ function AIRBOSS:_MarshalAI(flight, nstack) local p0=nil --Core.Point#COORDINATE if flight.case==1 then c1=p1 - p0=self:GetCoordinate():Translate(UTILS.NMToMeters(5), -90):SetAltitude(Altitude) + c2=p2 + p0=p1 --self:GetCoordinate():Translate(UTILS.NMToMeters(5), -90):SetAltitude(Altitude) + p0=self:GetCoordinate():Translate(UTILS.NMToMeters(2.5/math.sqrt(2)), 225):SetAltitude(Altitude) + --p0=self:GetCoordinate():Translate(UTILS.NMToMeters(2), hdg+190):SetAltitude(Altitude) else c1=p2 c2=p1 @@ -2899,16 +2934,19 @@ function AIRBOSS:_MarshalAI(flight, nstack) local text=string.format("Flight %s: Marshal stack %d: alt=%d, dist=%.1f, speed=%d", flight.groupname, stack, UTILS.MetersToFeet(Altitude), UTILS.MetersToNM(Dist), UTILS.MpsToKnots(SpeedOrbit)) -- Debug mark. - if self.Debug then - c1:MarkToAll(text) + if self.Debug or true then + --c1:MarkToAll(text) if c2 then - c2:MarkToAll(text) + --c2:MarkToAll(text) end end + p0:MarkToAll("p0") + p1:MarkToAll("p1") + p2:MarkToAll("p2") -- Waypoint. -- TODO: p0? - wp[#wp+1]=p1:WaypointAirTurningPoint(nil, SpeedTransit, {TaskOrbit}, text) + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, SpeedTransit, {TaskOrbit}, text) end @@ -2986,6 +3024,9 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) -- Center of holding pattern point. We give it a little head start -70 instead of -90 degrees. p1=Carrier:Translate(Dist, hdg-45) + + p1=Carrier:Translate(UTILS.NMToMeters(1.0), hdg) + p2=Carrier:Translate(UTILS.NMToMeters(3.5), hdg) else -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. angels0=6 @@ -3086,7 +3127,7 @@ end -- @param #AIRBOSS.FlightGroup flight Flight that left the marshal stack. -- @param #boolean nopattern If true, flight does not go to pattern. function AIRBOSS:_CollapseMarshalStack(flight, nopattern) - self:I({flight=flight, nopattern=nopattern}) + self:F2({flight=flight, nopattern=nopattern}) -- Recovery case of flight. local case=flight.case @@ -3115,7 +3156,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- Inform players. if mflight.ai==false and mflight.difficulty~=AIRBOSS.Difficulty.HARD then - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(mstack-1,case)) + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(mstack-1, case)) local text=string.format("descent to next lower stack at %d ft", alt) self:MessageToPlayer(mflight, text, "MARSHAL") end @@ -4024,6 +4065,8 @@ function AIRBOSS:OnEventLand(EventData) local airbase=EventData.Place local airbasename=tostring(airbase:GetName()) + -- TODO: also check distance to airbase since landing "in the water" also trigger a landing event! + -- Check if player landed on the right airbase. if airbasename==self.airbase:GetName() then @@ -4184,7 +4227,7 @@ function AIRBOSS:_Holding(playerData) local stack=playerData.flag:Get() -- Pattern alitude. - local patternalt, c1, c2=self:_GetMarshalAltitude(stack, playerData.case) + local patternalt=self:_GetMarshalAltitude(stack, playerData.case) -- Player altitude. local playeralt=unit:GetAltitude() @@ -6805,7 +6848,7 @@ function AIRBOSS:_GetOnboardNumbers(group, playeronly) end -- Debug info. - self:I(self.lid..text) + self:T2(self.lid..text) return numbers end @@ -6878,7 +6921,7 @@ function AIRBOSS:_GetFuelState(unit) local fuelstate=fuel*maxfuel -- Debug info. - self:I(self.lid..string.format("Unit %s fuel state = %.1f kg = %.1f lbs", unit:GetName(), fuelstate, UTILS.kg2lbs(fuelstate))) + self:T2(self.lid..string.format("Unit %s fuel state = %.1f kg = %.1f lbs", unit:GetName(), fuelstate, UTILS.kg2lbs(fuelstate))) return UTILS.kg2lbs(fuelstate) end @@ -6919,7 +6962,7 @@ function AIRBOSS:_GetUnitMasses(unit) local masscargo=massmax-massfuel-massempty -- Debug info. - self:I(self.lid..string.format("Unit %s mass fuel=%.1f kg, empty=%.1f kg, max=%.1f kg, cargo=%.1f kg", unit:GetName(), massfuel, massempty, massmax, masscargo)) + self:T2(self.lid..string.format("Unit %s mass fuel=%.1f kg, empty=%.1f kg, max=%.1f kg, cargo=%.1f kg", unit:GetName(), massfuel, massempty, massmax, masscargo)) return massfuel, massempty, massmax, masscargo end From 1559f14f11220dcf318c754831fb0739c63e37c6 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 15 Dec 2018 23:06:36 +0100 Subject: [PATCH 103/147] RESCUEHELO v0.9.7 - Fixed spawn bug with multiple helos. - Adjusted default parameters. - Changed RTB to respawn (not perfect). - Improved documentation. --- Moose Development/Moose/Core/Point.lua | 40 +-- Moose Development/Moose/Ops/RescueHelo.lua | 285 +++++++++++++++------ Moose Development/Moose/Wrapper/Group.lua | 137 +++++++--- 3 files changed, 332 insertions(+), 130 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 366ad03e1..ee5af0965 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1002,11 +1002,15 @@ do -- COORDINATE function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked, airbase, DCSTasks, description ) self:F2( { AltType, Type, Action, Speed, SpeedLocked } ) - -- Defaults + -- Set alttype or "RADIO" which is AGL. AltType=AltType or "RADIO" + + -- Speedlocked by default if SpeedLocked==nil then SpeedLocked=true end + + -- Speed or default 500 km/h. Speed=Speed or 500 -- Waypoint array. @@ -1015,19 +1019,26 @@ do -- COORDINATE -- Coordinates. RoutePoint.x = self.x RoutePoint.y = self.z + -- Altitude. RoutePoint.alt = self.y RoutePoint.alt_type = AltType + -- Waypoint type. RoutePoint.type = Type or nil RoutePoint.action = Action or nil - -- Set speed/ETA. + + -- Speed. RoutePoint.speed = Speed/3.6 RoutePoint.speed_locked = SpeedLocked + + -- ETA. RoutePoint.ETA=nil - RoutePoint.ETA_locked = false + RoutePoint.ETA_locked = false + -- Waypoint description. RoutePoint.name=description + -- Airbase parameters for takeoff and landing points. if airbase then local AirbaseID = airbase:GetID() @@ -1036,31 +1047,24 @@ do -- COORDINATE RoutePoint.linkUnit = AirbaseID RoutePoint.helipadId = AirbaseID elseif AirbaseCategory == Airbase.Category.AIRDROME then - RoutePoint.airdromeId = AirbaseID + RoutePoint.airdromeId = AirbaseID else self:T("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!") - end - end + end + + self:MarkToAll(string.format("Landing waypoint at airbase %s", airbase:GetName())) + end - - -- ["task"] = - -- { - -- ["id"] = "ComboTask", - -- ["params"] = - -- { - -- ["tasks"] = - -- { - -- }, -- end of ["tasks"] - -- }, -- end of ["params"] - -- }, -- end of ["task"] - -- Waypoint tasks. RoutePoint.task = {} RoutePoint.task.id = "ComboTask" RoutePoint.task.params = {} RoutePoint.task.params.tasks = DCSTasks or {} + -- Debug. self:T({RoutePoint=RoutePoint}) + + -- Return waypoint. return RoutePoint end diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 71d67d596..86434721b 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -13,7 +13,8 @@ -- -- === -- --- ### Author: **funkyfranky** +-- ### Author: **funkyfranky** +-- ### Contributions: Flightcontrol (@{#AI_FORMATION} class) -- -- @module Ops.RescueHelo -- @image MOOSE.JPG @@ -26,6 +27,7 @@ -- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. -- @field #string carriertype Carrier type. -- @field #string helogroupname Name of the late activated helo template group. +-- @field #string helogroupalias Spawn alias name of the group. Necessary for multiple RESCUEHELO objects in one mission. Uses groupname plus carrier name. -- @field Wrapper.Group#GROUP helo Helo group. -- @field #number takeoff Takeoff type. -- @field Wrapper.Airbase#AIRBASE airbase The airbase object acting as home base of the helo. @@ -45,6 +47,7 @@ -- @field #boolean rescuestopboat If true, stop carrier during rescue operations. -- @field #boolean carrierstop If true, route of carrier was stopped. -- @field #number HeloFuel0 Initial fuel of helo in percent. Necessary due to DCS bug that helo with full tank does not return fuel via API function. +-- @field #boolean rtb If true, Helo will be return to base on the next status check. -- @extends Core.Fsm#FSM --- Rescue Helo @@ -77,13 +80,13 @@ -- -- Once the helo is out of fuel, it will return to the carrier. When the helo lands, it will be respawned immidiately and go back on station. -- --- If a unit crashes or a pilot ejects within a radius of 100 km from the USS Stennis, the helo will automatically fly to the crash side and +-- If a unit crashes or a pilot ejects within a radius of 30 km from the USS Stennis, the helo will automatically fly to the crash side and -- rescue to pilot. This will take around 5 minutes. After that, the helo will return to the Stennis, land there and bring back the poor guy. -- When this is done, the helo will go back on station. -- -- # Fine Tuning -- --- The implementation allows to customize quite a few settings easily +-- The implementation allows to customize quite a few settings easily. -- -- ## Takeoff Type -- @@ -129,7 +132,22 @@ -- -- * @{#RESCUEHELO.SetAltitude}(*altitude*), where *altitude* is the altitude the helo flies at in meters. Default is 70 meters. -- * @{#RESCUEHELO.SetOffsetX}(*distance*)}, where *distance is the distance in the direction of movement of the carrier. Default is 200 meters. --- * @{#RESCUEHELO.SetOffsetZ}(*distance*)}, where *distance is the distance on the starboard side. Default is 200 meters. +-- * @{#RESCUEHELO.SetOffsetZ}(*distance*)}, where *distance is the distance on the starboard side. Default is 100 meters. +-- +-- ## Rescue Operations +-- +-- By default the rescue helo will start a rescue operation if an aircraft crashes or a pilot ejects in the vicinity of the carrier. +-- The standard "rescue zone" has a radius of 30 km around the carrier. The radius can be adjusted via the @{#RESCUEHELO.SetRescueZone}(*radius*) functions, +-- where *radius* is the radius of the zone in kilometers. If you use multiple rescue helos in the same mission, you might want to ensure that the radii +-- are not overlapping so that two helos try to rescue the same pilot. But it should not hurt either way. +-- +-- Once the helo reaches the crash site, the rescue operation will last 5 minutes. This time can be changed by @{#RESCUEHELO.SetRescueDuration(*time*), +-- where *time* is the duration in minutes. +-- +-- During the rescue operation, the helo will hover (orbit) over the crash site at a speed of 10 km/h. The speed can be set by @{#RESCUEHELO.SetRescueHoverSpeed}(*speed*), +-- where the *speed* is given in km/h. +-- +-- If no rescue operations should be carried out by the helo, this option can be completely disabled by using @{#RESCUEHELO.SetRescueOff}(). -- -- # Finite State Machine -- @@ -164,6 +182,7 @@ -- You have the option to enable the debug mode for this class via the @{#RESCUEHELO.SetDebugModeON} function. -- If enabled, text messages about the helo status will be displayed on screen and marks of the pattern created on the F10 map. -- +-- -- @field #RESCUEHELO RESCUEHELO = { ClassName = "RESCUEHELO", @@ -172,6 +191,7 @@ RESCUEHELO = { carrier = nil, carriertype = nil, helogroupname = nil, + helogroupalias = nil, helo = nil, airbase = nil, takeoff = nil, @@ -190,17 +210,19 @@ RESCUEHELO = { rescuespeed = nil, rescuestopboat = nil, HeloFuel0 = nil, - carrierstop = false, + rtb = nil, + carrierstop = nil, } --- Class version. -- @field #string version -RESCUEHELO.version="0.9.6" +RESCUEHELO.version="0.9.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Add messages for rescue mission. -- TODO: Add option to stop carrier while rescue operation is in progress? Done but NOT working! -- DONE: Write documenation. -- DONE: Add option to deactivate the rescueing. @@ -250,7 +272,17 @@ function RESCUEHELO:New(carrierunit, helogroupname) self:SetRescueHoverSpeed() self:SetRescueDuration() self:SetRescueStopBoatOff() - + + -- Some more. + self.rtb=false + self.carrierstop=false + + --[[ + BASE:TraceOnOff(true) + BASE:TraceClass("RESCUEHELO") + BASE:TraceLevel(1) + ]] + ----------------------- --- FSM Transitions --- ----------------------- @@ -263,6 +295,7 @@ function RESCUEHELO:New(carrierunit, helogroupname) self:AddTransition("Stopped", "Start", "Running") self:AddTransition("Running", "Rescue", "Rescuing") self:AddTransition("Running", "RTB", "Returning") + self:AddTransition("Rescuing", "RTB", "Returning") self:AddTransition("*", "Run", "Running") self:AddTransition("*", "Status", "*") self:AddTransition("*", "Stop", "Stopped") @@ -301,11 +334,13 @@ function RESCUEHELO:New(carrierunit, helogroupname) --- Triggers the FSM event "RTB" that sends the helo home. -- @function [parent=#RESCUEHELO] RTB -- @param #RESCUEHELO self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. --- Triggers the FSM event "RTB" that sends the helo home after a delay. -- @function [parent=#RESCUEHELO] __RTB -- @param #RESCUEHELO self -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. --- On after "RTB" event user function. Called when a the the helo returns to its home base. -- @function [parent=#RESCUEHELO] OnAfterRTB @@ -313,6 +348,7 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. --- Triggers the FSM event "Run". @@ -369,21 +405,22 @@ function RESCUEHELO:SetHomeBase(airbase) return self end ---- Set rescue zone radius. Crashed or ejected units inside this radius of the carrier will be rescued. +--- Set rescue zone radius. Crashed or ejected units inside this radius of the carrier will be rescued if possible. -- @param #RESCUEHELO self --- @param #number radius Radius of rescue zone in meters. Default is 100000 m = 100 km. +-- @param #number radius Radius of rescue zone in kilometers. Default is 30 km. -- @return #RESCUEHELO self function RESCUEHELO:SetRescueZone(radius) - self.rescuezone=ZONE_UNIT:New("Rescue Zone", self.carrier, radius or 100000) + radius=(radius or 30)*1000 + self.rescuezone=ZONE_UNIT:New("Rescue Zone", self.carrier, radius) return self end --- Set rescue hover speed. -- @param #RESCUEHELO self --- @param #number speed Speed in km/h. Default 25 km/h. +-- @param #number speed Speed in km/h. Default 10 km/h. -- @return #RESCUEHELO self function RESCUEHELO:SetRescueHoverSpeed(speed) - self.rescuespeed=UTILS.KmphToMps(speed or 25) + self.rescuespeed=UTILS.KmphToMps(speed or 10) return self end @@ -471,21 +508,21 @@ function RESCUEHELO:SetAltitude(alt) return self end ---- Set latitudinal offset to carrier. +--- Set offset parallel to orienation of carrier. -- @param #RESCUEHELO self --- @param #number distance Latitual offset distance in meters. Default 200 m. +-- @param #number distance Offset distance in meters. Default 200 m. -- @return #RESCUEHELO self function RESCUEHELO:SetOffsetX(distance) self.offsetX=distance or 200 return self end ---- Set longitudal offset to carrier. +--- Set offset perpendicular to orientation to carrier. -- @param #RESCUEHELO self --- @param #number distance Longitual offset distance in meters. Default 200 m. +-- @param #number distance Offset distance in meters. Default 100 m. -- @return #RESCUEHELO self function RESCUEHELO:SetOffsetZ(distance) - self.offsetZ=distance or 200 + self.offsetZ=distance or 100 return self end @@ -576,6 +613,13 @@ function RESCUEHELO:IsRescuing() return self:is("Rescuing") end +--- Check if FMS was stopped. +-- @param #RESCUEHELO self +-- @return #boolean If true, is stopped. +function RESCUEHELO:IsStopped() + return self:is("Stopped") +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- EVENT functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -587,18 +631,39 @@ function RESCUEHELO:OnEventLand(EventData) local group=EventData.IniGroup --Wrapper.Group#GROUP if group:IsAlive() then + + -- Group name that landed. local groupname=group:GetName() - if groupname:match(self.helogroupname) then + -- Check that it was our helo that landed. + if groupname==self.helo:GetName() then -- Respawn the Helo. local text=string.format("Respawning rescue helo group %s at home base.", groupname) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) self:T(self.lid..text) + if self:IsRescuing() then + + self:T(string.format("Rescue helo %s returned from rescue operation.", groupname)) + + end + if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then - self:E("ERROR: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true!", groupname) + if self:IsRescuing() then + + self:T(string.format("Rescue helo %s returned from rescue operation.", groupname)) + + else + + self:T2(string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true unless a rescue operation finished.", groupname)) + + end + + -- Respawn helo at current airbase anyway. + self.helo=group:RespawnAtCurrentAirbase() + else @@ -609,6 +674,7 @@ function RESCUEHELO:OnEventLand(EventData) -- Restart the formation. self:__Run(10) + end end end @@ -686,10 +752,13 @@ function RESCUEHELO:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Ejection, self._OnEventCrashOrEject) -- Delay before formation is started. - local delay=120 + local delay=120 - -- Spawn helo. - local Spawn=SPAWN:New(self.helogroupname):InitUnControlled(false) + -- Set unique alias for spawn. + self.helogroupalias=string.format("%s_%s", self.helogroupname, self.carrier:GetName()) + + -- Spawn helo. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. + local Spawn=SPAWN:NewWithAlias(self.helogroupname, self.helogroupalias) -- Spawn in air or at airbase. if self.takeoff==SPAWN.Takeoff.Air then @@ -697,11 +766,11 @@ function RESCUEHELO:onafterStart(From, Event, To) -- Carrier heading local hdg=self.carrier:GetHeading() - -- Spawn distance behind carrier. + -- Spawn distance in front of carrier. local dist=UTILS.NMToMeters(0.2) - -- Coordinate behind the carrier - local Carrier=self.carrier:GetCoordinate():SetAltitude(math.min(100, self.altitude)):Translate(dist, hdg) + -- Coordinate behind the carrier. Altitude at least 100 meters for spawning because it drops down a bit. + local Carrier=self.carrier:GetCoordinate():SetAltitude(math.max(140, self.altitude)):Translate(dist, hdg) -- Orientation of spawned group. Spawn:InitHeading(hdg) @@ -720,6 +789,9 @@ function RESCUEHELO:onafterStart(From, Event, To) -- Use an uncontrolled aircraft group. self.helo=GROUP:FindByName(self.helogroupname) + -- Also set the alias just in case. + self.helogroupalias=self.helogroupname + if self.helo:IsAlive() then -- Start uncontrolled group. @@ -770,7 +842,6 @@ function RESCUEHELO:onafterStart(From, Event, To) -- Init status check self:__Status(1) - end --- On after Status event. Checks player status. @@ -791,13 +862,50 @@ function RESCUEHELO:onafterStatus(From, Event, To) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) self:T(self.lid..text) - -- If fuel < threshold ==> send helo to home base! - if fuel Date: Sun, 16 Dec 2018 16:59:53 +0100 Subject: [PATCH 104/147] RECOVERYTANKER v0.9.9 RESCUEHELO v0.9.8 --- Moose Development/Moose/Core/Point.lua | 2 +- .../Moose/Functional/Warehouse.lua | 2 +- Moose Development/Moose/Ops/Airboss.lua | 63 ++-- .../Moose/Ops/RecoveryTanker.lua | 282 ++++++++++-------- Moose Development/Moose/Ops/RescueHelo.lua | 32 +- Moose Development/Moose/Wrapper/Group.lua | 3 +- 6 files changed, 205 insertions(+), 179 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index ee5af0965..a59abbce7 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1052,7 +1052,7 @@ do -- COORDINATE self:T("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!") end - self:MarkToAll(string.format("Landing waypoint at airbase %s", airbase:GetName())) + --self:MarkToAll(string.format("Landing waypoint at airbase %s", airbase:GetName())) end -- Waypoint tasks. diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index b0a49d6e6..e95ccb6ec 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -35,7 +35,7 @@ -- === -- -- @module Functional.Warehouse --- @image MOOSE.JPG +-- @image Warehouse.JPG --- WAREHOUSE class. -- @type WAREHOUSE diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index abd09ca20..6cf938fe6 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -390,9 +390,9 @@ -- -- # AI Handling -- --- The implementation allows to handle incoming AI units and integrate them into the marshal and landing pattern. +-- The AIRBOSS class allows to handle incoming AI units and integrate them into the marshal and landing pattern. -- --- By default, incoming carrier capable aircraft which are detecting inside the CCZ and approach the carrier by more than 5 NM are automatically guided to the holding zone. +-- By default, incoming carrier capable aircraft which are detecting inside the CCZ and approach the carrier by more than 10 NM are automatically guided to the holding zone. -- Each AI group gets its own marshal stack in the holding pattern. Once a recovery window opens, the AI group of the lowest stack is transitioning to the landing pattern -- and the Marshal stack collapses. -- @@ -1767,9 +1767,11 @@ function AIRBOSS:_CheckAIStatus() -- Pilot: "405, Hornet Ball, 3.2" -- TODO: Voice over. - local text=string.format("%s Ball, %.1f.", self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) + local text=string.format("%s Ball, %.1f.", self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) self:MessageToPattern(text, element.onboard, "", 3, false, 0, true) - MESSAGE:New(text, 15):ToAll() + + -- Debug message. + MESSAGE:New(string.format("%s, %s", element.onboard..text), 15, "DEBUG"):ToAllIf(self.Debug) -- Paddles: Roger ball after 3 seconds. self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 3) @@ -2749,13 +2751,13 @@ function AIRBOSS:_ScanCarrierZone() -- Debug info. self:T3(self.lid..string.format("Known AI flight group %s closed in by %.1f NM", knownflight.groupname, UTILS.MetersToNM(closein))) - -- Send AI flight to marshal stack if group closes in more than 2.5 and has initial flag value. - if closein>UTILS.NMToMeters(2.5) and knownflight.flag:Get()==-100 then + -- Send AI flight to marshal stack if group closes in more than 5 and has initial flag value. + if closein>UTILS.NMToMeters(5) and knownflight.flag:Get()==-100 then -- Check that we do not add a recovery tanker for marshaling. if self.tanker and self.tanker.tanker:GetName()==groupname then - -- Don't touch the recovery thanker! + -- Don't touch the recovery tanker! else @@ -2788,7 +2790,7 @@ function AIRBOSS:_ScanCarrierZone() end end - -- Remove flight groups. + -- Remove flight groups outside CCA. for _,group in pairs(remove) do self:_RemoveFlightGroup(group) end @@ -5152,17 +5154,20 @@ end -- @param #number dx Correction. function AIRBOSS:_GetWire(Ccoord, Lcoord, dx) + -- Heading of carrier (true). local hdg=self.carrier:GetHeading() - -- Stern coordinate (sterndist<0) - local Scoord=Ccoord:Translate(self.carrierparam.sterndist, hdg) + -- Final bearing (true). + local FB=self:GetFinalBearing() - -- Distance to landing coord + -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. + local Scoord=Ccoord:Translate(self.carrierparam.sterndist, hdg):Translate(10, FB+90) + + -- Distance to landing coord. local Ldist=Lcoord:Get2DDistance(Scoord) -- Little offset for the exact wire positions. - dx=dx or self.carrierparam.wireoffset - + -- TODO: Maybe add little offset depending on aircraft type. dx=self.carrierparam.wireoffset -- Corrected distance. @@ -5183,8 +5188,7 @@ function AIRBOSS:_GetWire(Ccoord, Lcoord, dx) end if self.Debug then - local FB=self:GetFinalBearing(false) - + local w1=Scoord:Translate(self.carrierparam.wire1+self.carrierparam.wireoffset, FB) local w2=Scoord:Translate(self.carrierparam.wire2+self.carrierparam.wireoffset, FB) local w3=Scoord:Translate(self.carrierparam.wire3+self.carrierparam.wireoffset, FB) @@ -7485,16 +7489,15 @@ function AIRBOSS:_AddF10Commands(_unitName) -- Get group and ID. local group=_unit:GetGroup() local gid=group:GetID() - - -- Player Data. - local playerData=self.players[playername] - - if group and gid and playerData then + + if group and gid then if not self.menuadded[gid] then -- Enable switch so we don't do this twice. self.menuadded[gid]=true + + env.info("FF menu") -- Main F10 menu: F10/Airboss// if AIRBOSS.MenuF10[gid]==nil then @@ -7551,10 +7554,10 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) -- F5 end else - self:T(self.lid.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) + self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) end else - self:T(self.lid.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) + self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName)) end end @@ -7712,12 +7715,7 @@ function AIRBOSS:_RequestCommence(_unitName) local text if _unit:IsInZone(self.zoneCCA) then - if self:_InQueue(self.Qmarshal, playerData.group) then - - -- Flight group is already in marhal queue. - text=string.format("%s, you are already in the Marshal queue. Commence request denied!", playerData.name) - - elseif self:_InQueue(self.Qpattern, playerData.group) then + if self:_InQueue(self.Qpattern, playerData.group) then -- Flight group is already in pattern queue. text=string.format("%s, you are already in the Pattern queue. Commence request denied!", playerData.name) @@ -7742,10 +7740,13 @@ function AIRBOSS:_RequestCommence(_unitName) local _,npattern=self:_GetQueueInfo(self.Qpattern) -- Check if pattern is already full. - if npattern>=self.Nmaxpattern then + if npattern>=self.Nmaxpattern then + -- Patern is full! text=string.format("Negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern) + else + -- Positive response. if playerData.case==1 then text="Proceed to initial." @@ -7861,9 +7862,9 @@ function AIRBOSS:_SetSection(_unitName) -- Check if player is in Marshal or pattern queue already. local text if self:_InQueue(self.Qmarshal,playerData.group) then - text=string.format("You are already in the Marshal queue. Setting section no possible any more!") + text=string.format("You are already in the Marshal queue. Setting section not possible any more!") elseif self:_InQueue(self.Qpattern, playerData.group) then - text=string.format("You are already in the Pattern queue. Setting section no possible any more!") + text=string.format("You are already in the Pattern queue. Setting section not possible any more!") else -- Init array diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 4896ec6b1..45e160e5f 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -1,14 +1,15 @@ ---- **Ops** - (R2.5) - Carrier recovery tanker. +--- **Ops** - (R2.5) - Recovery tanker for carrier operations. -- -- Tanker aircraft flying a racetrack pattern overhead an aircraft carrier. -- -- **Main Features:** -- -- * Regular pattern update with respect to carrier positon. +-- * No restrictions regarding carrier waypoints and heading. -- * Automatic respawning when tanker runs out of fuel for 24/7 operations. -- * Tanker can be spawned cold or hot on the carrier or at any other airbase or directly in air. -- * Automatic AA TACAN beacon setting. --- * Multiple tanker at different carriers due to object oriented approach. +-- * Multiple tankers at different carriers due to object oriented approach. -- * Finite State Machine (FSM) implementation, which allows the mission designer to hook into certain events. -- -- === @@ -36,8 +37,8 @@ -- @field #boolean TACANon If true, TACAN is automatically activated. If false, TACAN is disabled. -- @field #number speed Tanker speed when flying pattern. -- @field #number altitude Tanker orbit pattern altitude. --- @field #number distStern Race-track distance astern. --- @field #number distBow Race-track distance bow. +-- @field #number distStern Race-track distance astern. distStern is <0. +-- @field #number distBow Race-track distance bow. distBow is >0. -- @field #number Dupdate Pattern update when carrier changes its position by more than this distance (meters). -- @field #number Hupdate Pattern update when carrier changes its heading by more than this number (degrees). -- @field #number dTupdate Minimum time interval in seconds before the next pattern update can happen. @@ -50,18 +51,17 @@ -- @field DCS#Vec3 orientation Orientation of the carrier. Used to monitor changes and update the pattern if heading changes significantly. -- @field DCS#Vec3 orientlast Orientation of the carrier for checking if carrier is currently turning. -- @field Core.Point#COORDINATE position Positon of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. --- @field Core.Zone#ZONE_UNIT zoneUpdate Moving zone relative to carrier. Each time the tanker is in this zone, its pattern is updated. -- @extends Core.Fsm#FSM --- Recovery Tanker. -- -- === -- --- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Main.jpg) +-- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Main.png) -- -- # Recovery Tanker -- --- A recovery tanker acts as refueling unit flying overhead an aircraft carrier in order to supply incoming flights with gas if they go "Bingo on the Ball". +-- A recovery tanker acts as refueling unit flying overhead an aircraft carrier in order to supply incoming flights with gas if they go "*Bingo on the Ball*". -- -- # Simple Script -- @@ -76,18 +76,25 @@ -- -- The first line will create a new RECOVERYTANKER object and the second line starts the process. -- --- With this setup, the tanker will be spawned on the USS Stennis with running engines. After it takes off, it will fly a position astern of the boat and from there start its +-- With this setup, the tanker will be spawned on the USS Stennis with running engines. After it takes off, it will fly a position ~10 NM astern of the boat and from there start its -- pattern. This is a counter clockwise racetrack pattern at angels 6. -- +-- A TACAN beacon will be automatically activated at channel 1Y with morse code "TKR". See below how to change this setting. +-- +-- Note that the Tanker entry in the F10 radio menu will appear once the tanker is on station and not before. If you spawn the tanker cold or hot on the carrier, this will take ~10 minutes. +-- +-- Also note, that currently the only carrier capable aircraft in DCS is the S-3B Viking (tanker version). If you want to use another refueling aircraft, you need to activate air spawn +-- or set a different land based airport of the map. This will be explained below. +-- -- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Pattern.jpg) -- -- The "downwind" leg of the pattern is normally used for refueling. -- --- Once the tanker runs out of fuel itself, it will return to the carrier and be respawned. +-- Once the tanker runs out of fuel itself, it will return to the carrier, respawn with full fuel and take up its pattern again. -- -- # Options and Fine Tuning -- --- Several parameters can be customized by the mission designer. +-- Several parameters can be customized by the mission designer via user API functions. -- -- ## Takeoff Type -- @@ -96,7 +103,7 @@ -- -- * @{#RECOVERYTANKER.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default. -- * @{#RECOVERYTANKER.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off. --- * @{#RECOVERYTANKER.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the tanker will be spawned in air relatively far behind the carrier. +-- * @{#RECOVERYTANKER.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the tanker will be spawned in air ~10 NM astern the carrier. -- -- For example, -- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") @@ -118,8 +125,18 @@ -- The racetrack pattern parameters can be fine tuned via the following functions: -- -- * @{#RECOVERYTANKER.SetAltitude}(*altitude*), where *altitude* is the pattern altitude in feet. Default 6000 ft. --- * @{#RECOVERYTANKER.SetSpeed}(*speed*), where *speed* is the pattern speed in knots. Default is 272 knots. --- * @{#RECOVERYTANKER.SetRacetrackDistances}(*distbow*, *diststern*), where *distbow* and *diststern* are the distances ahead and astern the boat, respectively. +-- * @{#RECOVERYTANKER.SetSpeed}(*speed*), where *speed* is the pattern speed in knots. Default is 274 knots TAS which results in ~250 KIAS. +-- * @{#RECOVERYTANKER.SetRacetrackDistances}(*distbow*, *diststern*), where *distbow* and *diststern* are the distances ahead and astern the boat (default 10 and 4 NM), respectively. +-- In principle, these number should be more like 8 and 6 NM but since the carrier is moving, we give translate the pattern points a bit forward. +-- +-- ## Home Base +-- +-- The home base is the airbase where the tanker is spawned (if not in air) and where it will go once it is running out of fuel. The default home base is the carrier itself. +-- The home base can be changed via the @{#RECOVERYTANKER.SetHomeBase}(*airbase*) function, where *airbase* can be a MOOSE @{Wrapper.Airbase#AIRBASE} object or simply the +-- name of the airbase passed as string. +-- +-- Note that only the S3B Viking is a refueling aircraft that is carrier capable. You can use other tanker aircraft types, e.g. the KC-130, but in this case you must either +-- set an airport of the map as home base or activate spawning in air via @{#RECOVERYTANKER.SetTakeoffAir}. -- -- ## TACAN -- @@ -141,14 +158,14 @@ -- -- The pattern of the tanker is updated if at least one of the two following conditions apply: -- --- * The aircraft carrier changes its position by more than ~10 km (see @{#RECOVERYTANKER.SetPatternUpdateDistance}) and/or +-- * The aircraft carrier changes its position by more than 5 NM (see @{#RECOVERYTANKER.SetPatternUpdateDistance}) and/or -- * The aircraft carrier changes its heading by more than 5 degrees (see @{#RECOVERYTANKER.SetPatternUpdateHeading}) -- --- **Note** that updating the pattern always leads to a small disruption in the perfect racetrack pattern of the tanker. This is because a new waypoint and new racetrack points --- need to be set as DCS task. This is also the reason why the pattern is not contantly updated but rather when the position or heading of the carrier changes significantly. +-- **Note** that updating the pattern often leads to a more or less small disruption of the perfect racetrack pattern of the tanker. This is because a new waypoint and new racetrack points +-- need to be set as DCS task. This is the reason why the pattern is not contantly updated but rather when the position or heading of the carrier changes significantly. -- -- The maximum update frequency is set to 10 minutes. You can adjust this by @{#RECOVERYTANKER.SetPatternUpdateInterval}. --- Also the pattern will not be updated while the carrier is turning or the tanker is currently refuelling another unit. +-- Also the pattern will not be updated whilst the carrier is turning or the tanker is currently refueling another unit. -- -- # Finite State Machine -- @@ -216,19 +233,18 @@ RECOVERYTANKER = { orientation = nil, orientlast = nil, position = nil, - zoneUpdate = nil, } --- Class version. -- @field #string version -RECOVERYTANKER.version="0.9.8" +RECOVERYTANKER.version="0.9.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Seamless change of position update. Get good updated waypoint and update position if tanker position is right! -- TODO: Is alive check for tanker necessary? +-- DONE: Seamless change of position update. Get good updated waypoint and update position if tanker position is right. Not really possiple atm. -- DONE: Check if TACAN mode "X" is allowed for AA TACAN stations. Nope -- DONE: Check if tanker is going back to "Running" state after RTB and respawn. -- DONE: Write documenation. @@ -275,7 +291,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- Init default parameters. self:SetAltitude() self:SetSpeed() - self:SetRacetrackDistances(6, 8) + self:SetRacetrackDistances() self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) self:SetTakeoffHot() self:SetLowFuelThreshold() @@ -285,10 +301,12 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetPatternUpdateHeading() self:SetPatternUpdateInterval() - -- Moving zone: Zone 1 NM astern the carrier with radius of 1 NM. - --self.zoneUpdate=ZONE_UNIT:New("Pattern Update Zone", self.carrier, UTILS.NMToMeters(1), {dx=-UTILS.NMToMeters(1), dy=0, relative_to_unit=true}) - --self.zoneUpdate:SmokeZone(SMOKECOLOR.White, 45) - + --[[ + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + ]] + ----------------------- --- FSM Transitions --- ----------------------- @@ -421,10 +439,10 @@ end --- Set the speed the tanker flys in its orbit pattern. -- @param #RECOVERYTANKER self --- @param #number speed Tanker speed in knots. Default 272 knots. +-- @param #number speed True air speed (TAS) in knots. Default 274 knots, which results in ~250 KIAS. -- @return #RECOVERYTANKER self function RECOVERYTANKER:SetSpeed(speed) - self.speed=UTILS.KnotsToMps(speed or 272) + self.speed=UTILS.KnotsToMps(speed or 274) return self end @@ -439,12 +457,12 @@ end --- Set race-track distances. -- @param #RECOVERYTANKER self --- @param #number distbow Distance [NM] in front of the carrier. Default 6 NM. --- @param #number diststern Distance [NM] behind the carrier. Default 8 NM. +-- @param #number distbow Distance [NM] in front of the carrier. Default 10 NM. +-- @param #number diststern Distance [NM] behind the carrier. Default 4 NM. -- @return #RECOVERYTANKER self function RECOVERYTANKER:SetRacetrackDistances(distbow, diststern) - self.distBow=UTILS.NMToMeters(distbow or 6) - self.distStern=-UTILS.NMToMeters(diststern or 8) + self.distBow=UTILS.NMToMeters(distbow or 10) + self.distStern=-UTILS.NMToMeters(diststern or 4) return self end @@ -457,16 +475,16 @@ function RECOVERYTANKER:SetPatternUpdateInterval(interval) return self end ---- Set pattern update distance. Tanker will update its pattern when the carrier changes its position by more than this distance. +--- Set pattern update distance threshold. Tanker will update its pattern when the carrier changes its position by more than this distance. -- @param #RECOVERYTANKER self --- @param #number distancechange Distance threshold in km. Default 9.62 km (= 5 NM). +-- @param #number distancechange Distance threshold in NM. Default 5 NM (=9.62 km). -- @return #RECOVERYTANKER self function RECOVERYTANKER:SetPatternUpdateDistance(distancechange) - self.Dupdate=(distancechange or 9.62)*1000 + self.Dupdate=UTILS.NMToMeters(distancechange or 5) return self end ---- Set pattern update heading. Tanker will update its pattern when the carrier changes its heading by more than this value. +--- Set pattern update heading threshold. Tanker will update its pattern when the carrier changes its heading by more than this value. -- @param #RECOVERYTANKER self -- @param #number headingchange Heading threshold in degrees. Default 5 degrees. -- @return #RECOVERYTANKER self @@ -477,19 +495,26 @@ end --- Set low fuel state of tanker. When fuel is below this threshold, the tanker will RTB or be respawned if takeoff type is in air. -- @param #RECOVERYTANKER self --- @param #number fuelthreshold Low fuel threshold in percent. Default 10 %. +-- @param #number fuelthreshold Low fuel threshold in percent. Default 10 % of max fuel. -- @return #RECOVERYTANKER self function RECOVERYTANKER:SetLowFuelThreshold(fuelthreshold) self.lowfuel=fuelthreshold or 10 return self end ---- Set home airbase of the tanker. Default is the carrier. +--- Set home airbase of the tanker. This is the airbase where the tanker will go when it is out of fuel. -- @param #RECOVERYTANKER self --- @param Wrapper.Airbase#AIRBASE airbase +-- @param Wrapper.Airbase#AIRBASE airbase The home airbase. Can be the airbase name or a Moose AIRBASE object. -- @return #RECOVERYTANKER self function RECOVERYTANKER:SetHomeBase(airbase) - self.airbase=airbase + if type(airbase)=="string" then + self.airbase=AIRBASE:FindByName(airbase) + else + self.airbase=airbase + end + if not self.airbase then + self:E(self.lid.."ERROR: Airbase is nil!") + end return self end @@ -518,7 +543,7 @@ function RECOVERYTANKER:SetTakeoffCold() return self end ---- Set takeoff in air at the defined pattern altitude and 20 NM astern the carrier. +--- Set takeoff in air at the defined pattern altitude and ~10 NM astern the carrier. -- @param #RECOVERYTANKER self -- @return #RECOVERYTANKER self function RECOVERYTANKER:SetTakeoffAir() @@ -566,7 +591,7 @@ function RECOVERYTANKER:SetRespawnInAir() end --- Use an uncontrolled aircraft already present in the mission rather than spawning a new tanker as initial recovery thanker. --- This can be useful when interfaced with, e.g., a warehouse. +-- This can be useful when interfaced with, e.g., a MOOSE @{Functional.Warehouse#WAREHOUSE}. -- The group name is the one specified in the @{#RECOVERYTANKER.New} function. -- @param #RECOVERYTANKER self -- @return #RECOVERYTANKER self @@ -597,7 +622,7 @@ function RECOVERYTANKER:SetTACAN(channel, morse) return self end ---- Activate debug mode. Marks of pattern on F10 map etc. +--- Activate debug mode. Marks of pattern on F10 map and debug messages displayed on screen. -- @param #RECOVERYTANKER self -- @return #RECOVERYTANKER self function RECOVERYTANKER:SetDebugModeON() @@ -660,8 +685,11 @@ function RECOVERYTANKER:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Refueling, self._RefuelingStart) --Need explcit functions sice OnEventRefueling and OnEventRefuelingStop did not hook. self:HandleEvent(EVENTS.RefuelingStop, self._RefuelingStop) - -- Spawn tanker. - local Spawn=SPAWN:New(self.tankergroupname):InitUnControlled(false) + -- Set unique alias for spawn from tanker group name and carrier unit name. + local tankergroupalias=string.format("%s_%s", self.tankergroupname, self.carrier:GetName()) + + -- Spawn tanker. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. + local Spawn=SPAWN:NewWithAlias(self.tankergroupname, tankergroupalias) -- Spawn on carrier. if self.takeoff==SPAWN.Takeoff.Air then @@ -670,13 +698,13 @@ function RECOVERYTANKER:onafterStart(From, Event, To) local hdg=self.carrier:GetHeading() -- Spawn distance behind the carrier. - local dist=UTILS.NMToMeters(20) + local dist=-self.distStern+UTILS.NMToMeters(4) - -- Coordinate behind the carrier - local Carrier=self.carrier:GetCoordinate():SetAltitude(self.altitude):Translate(-dist, hdg) + -- Coordinate behind the carrier and slightly port. + local Carrier=self.carrier:GetCoordinate():SetAltitude(self.altitude):Translate(dist, hdg+190) -- Orientation of spawned group. - Spawn:InitHeading(hdg) + Spawn:InitHeading(hdg+10) -- Spawn at coordinate. self.tanker=Spawn:SpawnFromCoordinate(Carrier) @@ -709,8 +737,9 @@ function RECOVERYTANKER:onafterStart(From, Event, To) end - -- Initialize route. - self:_InitRoute(15, 1) + -- Initialize route. self.distStern<0! + SCHEDULER:New(self, self._InitRoute, {-self.distStern+UTILS.NMToMeters(3)}, 1) + --self:_InitRoute(-self.distStern+UTILS.NMToMeters(3), 1) -- Create tanker beacon. if self.TACANon then @@ -741,19 +770,6 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) local fuel=self.tanker:GetFuel()*100 local text=string.format("Recovery tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) self:T(self.lid..text) - - -- Check if tanker flies through pattern update zone. - -- TODO: Check if this can be used to update the pattern without too much disruption. - -- Could be a problem when carrier changes course since the tanker might not fligh through the zone any more. - --[[ - if self.Debug and self.zoneUpdate then - local inupdatezone=self.tanker:GetUnit(1):IsInZone(self.zoneUpdate) - if inupdatezone then - local clock=UTILS.SecondsToClock(timer.getAbsTime()) - self:T(string.format("Recovery tanker is in pattern update zone! Time=%s", clock)) - end - end - ]] -- Check if tanker is running and not RTBing or refueling. if self:IsRunning() then @@ -837,7 +853,9 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) local Carrier=self.carrier:GetCoordinate() -- Define race-track pattern. - local p0=self.tanker:GetCoordinate():Translate(3000, self.tanker:GetHeading()) + local p0=self.tanker:GetCoordinate():Translate(UTILS.NMToMeters(1), self.tanker:GetHeading()) + + -- Racetrack pattern points. local p1=Carrier:SetAltitude(self.altitude):Translate(self.distStern, hdg) local p2=Carrier:SetAltitude(self.altitude):Translate(self.distBow, hdg) @@ -858,8 +876,6 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil , UTILS.MpsToKmph(self.speed), {}, "Current Position") wp[2]=p0:WaypointAirTurningPoint(nil, UTILS.MpsToKmph(self.speed), {taskorbit}, "Tanker Orbit") - --local wp=self:_Pattern() - -- Initialize WP and route tanker. self.tanker:WayPointInitialize(wp) @@ -876,47 +892,6 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) self.Tupdate=timer.getTime() end - ---- Self made race track pattern. (not used) --- @param #RECOVERYTANKER self --- @return #table Table of pattern waypoints. -function RECOVERYTANKER:_Pattern() - - -- Carrier heading. - local hdg=self.carrier:GetHeading() - - -- Pattern altitude - local alt=self.altitude - - -- Carrier position. - local Carrier=self.carrier:GetCoordinate() - - local width=UTILS.NMToMeters(8) - - -- Not working as desired, since tanker changes course too rapidly after each waypoint. - - -- Define race-track pattern. - local p={} - p[1]=self.tanker:GetCoordinate() -- Tanker position - p[2]=Carrier:SetAltitude(alt) -- Carrier position - p[3]=p[2]:Translate(self.distBow, hdg) -- In front of carrier - p[4]=p[3]:Translate(width/math.sqrt(2), hdg-45) -- Middle front for smoother curve - -- Probably need one more to make it go -hdg at the waypoint. - p[5]=p[3]:Translate(width, hdg-90) -- In front on port - p[6]=p[5]:Translate(self.distStern-self.distBow, hdg) -- Behind on port (sterndist<0!) - p[7]=p[2]:Translate(self.distStern, hdg) -- Behind carrier - - local wp={} - for i=1,#p do - local coord=p[i] --Core.Point#COORDINATE - coord:MarkToAll(string.format("Waypoint %d", i)) - --table.insert(wp, coord:WaypointAirFlyOverPoint(nil , self.speed)) - table.insert(wp, coord:WaypointAirTurningPoint(nil , UTILS.MpsToKmph(self.speed))) - end - - return wp -end - --- On after "RTB" event. Send tanker back to carrier. -- @param #RECOVERYTANKER self -- @param #string From From state. @@ -929,16 +904,19 @@ function RECOVERYTANKER:onafterRTB(From, Event, To, airbase) airbase=airbase or self.airbase -- Debug message. - local text=string.format("Recoery tanker %s returning to airbase %s.", self.tanker:GetName(), airbase:GetName()) + local text=string.format("Recovery tanker %s returning to airbase %s.", self.tanker:GetName(), airbase:GetName()) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) self:T(self.lid..text) -- Waypoint array. local wp={} + -- Set speed ot 75% max. + local speed=self.tanker:GetSpeedMax()*0.75 + -- Set landing waypoint. - wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil, 300, {}, "Current Position") - wp[2]=airbase:GetCoordinate():SetAltitude(500):WaypointAirLanding(300, airbase, nil, "Land at airbase") + wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil, speed, {}, "Current Position") + wp[2]=airbase:GetCoordinate():SetAltitude(500):WaypointAirLanding(speed, airbase, nil, "Land at airbase") -- Initialize WP and route tanker. self.tanker:WayPointInitialize(wp) @@ -956,7 +934,6 @@ function RECOVERYTANKER:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.EngineShutdown) self:UnHandleEvent(EVENTS.Refueling) self:UnHandleEvent(EVENTS.RefuelingStop) - --self:UnHandleEvent(EVENTS.Land) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -969,22 +946,23 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function RECOVERYTANKER:OnEventEngineShutdown(EventData) + -- Group that shut down the engine. local group=EventData.IniGroup --Wrapper.Group#GROUP - -- Check if group is alive and should be respawned. - if group:IsAlive() and self.respawn then + -- Check if group is alive. + if group:IsAlive() then -- Group name. When spawning it will have #001 attached. local groupname=group:GetName() - if groupname:match(self.tankergroupname) then + -- Check that we have the right group and that it should be respawned. + if groupname==self.tanker:GetName() and self.respawn then -- Debug info. local text=string.format("Respawning recovery tanker group %s.", group:GetName()) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) self:T(self.lid..text) - - + -- Respawn tanker. self.tanker=group:RespawnAtCurrentAirbase() @@ -994,7 +972,8 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) end -- Initial route. - self:_InitRoute(15, 1) + SCHEDULER:New(self, self._InitRoute, {-self.distStern+UTILS.NMToMeters(3)}, 1) + --self:_InitRoute(-self.distStern+UTILS.NMToMeters(3), 1) end end @@ -1084,14 +1063,14 @@ function RECOVERYTANKER:_InitPatternTaskFunction() end ---- Init waypoint after spawn. +--- Init waypoint after spawn. Tanker is first guided to a position astern the carrier and starts its racetrack pattern from there. -- @param #RECOVERYTANKER self --- @param #number dist Distance [NM] of initial waypoint astern carrier. Default 15 NM. +-- @param #number dist Distance [NM] of initial waypoint astern carrier. Default 8 NM. -- @param #number delay Delay before routing in seconds. Default 1 second. function RECOVERYTANKER:_InitRoute(dist, delay) -- Defaults. - dist=UTILS.NMToMeters(dist or 15) + dist=dist or UTILS.NMToMeters(8) delay=delay or 1 -- Debug message. @@ -1103,12 +1082,19 @@ function RECOVERYTANKER:_InitRoute(dist, delay) -- Carrier heading. local hdg=self.carrier:GetHeading() - -- First waypoint is ~15 NM behind the boat. - local p=Carrier:Translate(-dist, hdg):SetAltitude(self.altitude) + -- First waypoint is ~10 NM behind and slightly port the boat. + local p=Carrier:Translate(dist, hdg+190):SetAltitude(self.altitude) + + -- Speed for waypoints in km/h. + -- This causes a problem, because the tanker might not be alive yet ==> We schedule the call of _InitRoute + local speed=self.tanker:GetSpeedMax()*0.8 + + -- Set to 280 knots and convert to km/h. + --local speed=280/0.539957 -- Debug mark. if self.Debug then - p:MarkToAll(string.format("Init WP: alt=%d ft, speed=%d kts", UTILS.MetersToFeet(self.altitude), UTILS.MpsToKnots(self.speed))) + p:MarkToAll(string.format("Enter Pattern WP: alt=%d ft, speed=%d kts", UTILS.MetersToFeet(self.altitude), speed*0.539957)) end -- Task to update pattern when wp 2 is reached. @@ -1117,11 +1103,11 @@ function RECOVERYTANKER:_InitRoute(dist, delay) -- Waypoints. local wp={} if self.takeoff==SPAWN.Takeoff.Air then - wp[#wp+1]=self.tanker:GetCoordinate():SetAltitude(self.altitude):WaypointAirTurningPoint(nil, UTILS.MpsToKmph(self.speed), {}, "Spawn Position") + wp[#wp+1]=self.tanker:GetCoordinate():SetAltitude(self.altitude):WaypointAirTurningPoint(nil, speed, {}, "Spawn Position") else wp[#wp+1]=Carrier:WaypointAirTakeOffParking() end - wp[#wp+1]=p:WaypointAirTurningPoint(nil, UTILS.MpsToKmph(self.speed), {task}, "Begin Pattern") + wp[#wp+1]=p:WaypointAirTurningPoint(nil, speed, {task}, "Enter Pattern") -- Set route. self.tanker:Route(wp, delay) @@ -1181,7 +1167,7 @@ function RECOVERYTANKER:_CheckPatternUpdate(dt) -- Get distance to saved position. local dist=pos:Get2DDistance(self.position) - -- Check if carrier moved more than ~10 km. + -- Check if carrier moved more than ~5 NM. local Dchange=false if dist>self.Dupdate then self:T(self.lid..string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) @@ -1191,13 +1177,13 @@ function RECOVERYTANKER:_CheckPatternUpdate(dt) -- Assume no update necessary. local update=false - -- No update if currently turning! Also must be running (not RTB or refuelling) and T>~10 min since last position update. + -- No update if currently turning! Also must be running (not RTB or refueling) and T>~10 min since last position update. if self:IsRunning() and dt>self.dTupdate and not turning then -- Update if heading or distance changed. if Hchange or Dchange then -- Debug message. - local text=string.format("Updating tanker %s pattern due to carrier change.", self.tanker:GetName()) + local text=string.format("Updating tanker %s pattern due to carrier position=%s or heading=%s change.", self.tanker:GetName(), tostring(Dchange), tostring(Hchange)) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) self:T(self.lid..text) @@ -1220,7 +1206,7 @@ function RECOVERYTANKER:_ActivateTACAN(delay) if delay and delay>0 then -- Schedule TACAN activation. - SCHEDULER:New(nil,self._ActivateTACAN, {self}, delay) + SCHEDULER:New(nil, self._ActivateTACAN, {self}, delay) else @@ -1247,6 +1233,44 @@ function RECOVERYTANKER:_ActivateTACAN(delay) end +--- Self made race track pattern. Not working as desired, since tanker changes course too rapidly after each waypoint. +-- @param #RECOVERYTANKER self +-- @return #table Table of pattern waypoints. +function RECOVERYTANKER:_Pattern() + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- Pattern altitude + local alt=self.altitude + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + local width=UTILS.NMToMeters(8) + + -- Define race-track pattern. + local p={} + p[1]=self.tanker:GetCoordinate() -- Tanker position + p[2]=Carrier:SetAltitude(alt) -- Carrier position + p[3]=p[2]:Translate(self.distBow, hdg) -- In front of carrier + p[4]=p[3]:Translate(width/math.sqrt(2), hdg-45) -- Middle front for smoother curve + -- Probably need one more to make it go -hdg at the waypoint. + p[5]=p[3]:Translate(width, hdg-90) -- In front on port + p[6]=p[5]:Translate(self.distStern-self.distBow, hdg) -- Behind on port (sterndist<0!) + p[7]=p[2]:Translate(self.distStern, hdg) -- Behind carrier + + local wp={} + for i=1,#p do + local coord=p[i] --Core.Point#COORDINATE + coord:MarkToAll(string.format("Waypoint %d", i)) + --table.insert(wp, coord:WaypointAirFlyOverPoint(nil , self.speed)) + table.insert(wp, coord:WaypointAirTurningPoint(nil , UTILS.MpsToKmph(self.speed))) + end + + return wp +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 86434721b..6413d1d64 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -5,16 +5,16 @@ -- **Main Features:** -- -- * Close formation with carrier. --- * Carrier can have any number of waypoints. +-- * No restrictions regarding carrier waypoints and heading. -- * Automatic respawning on empty fuel for 24/7 operations. --- * Automatic rescuing of crashed or ejected units in the vicinity. +-- * Automatic rescuing of crashed or ejected pilots in the vicinity of the carrier. -- * Multiple helos at different carriers due to object oriented approach. -- * Finite State Machine (FSM) implementation. -- -- === -- -- ### Author: **funkyfranky** --- ### Contributions: Flightcontrol (@{#AI_FORMATION} class) +-- ### Contributions: Flightcontrol (@{AI.#AI_FORMATION} class) -- -- @module Ops.RescueHelo -- @image MOOSE.JPG @@ -27,7 +27,6 @@ -- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. -- @field #string carriertype Carrier type. -- @field #string helogroupname Name of the late activated helo template group. --- @field #string helogroupalias Spawn alias name of the group. Necessary for multiple RESCUEHELO objects in one mission. Uses groupname plus carrier name. -- @field Wrapper.Group#GROUP helo Helo group. -- @field #number takeoff Takeoff type. -- @field Wrapper.Airbase#AIRBASE airbase The airbase object acting as home base of the helo. @@ -54,7 +53,7 @@ -- -- === -- --- ![Banner Image](..\Presentations\RESCUEHELO\RescueHelo_Main.jpg) +-- ![Banner Image](..\Presentations\RESCUEHELO\RescueHelo_Main.png) -- -- # Recue Helo -- @@ -191,7 +190,6 @@ RESCUEHELO = { carrier = nil, carriertype = nil, helogroupname = nil, - helogroupalias = nil, helo = nil, airbase = nil, takeoff = nil, @@ -216,7 +214,7 @@ RESCUEHELO = { --- Class version. -- @field #string version -RESCUEHELO.version="0.9.7" +RESCUEHELO.version="0.9.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -396,12 +394,19 @@ function RESCUEHELO:SetLowFuelThreshold(threshold) return self end ---- Set home airbase of the helo. Default is the carrier. +--- Set home airbase of the helo. This is the airbase where the helo is spawned (if not in air) and will go when it is out of fuel. -- @param #RESCUEHELO self --- @param Wrapper.Airbase#AIRBASE airbase Homebase of helo. +-- @param Wrapper.Airbase#AIRBASE airbase The home airbase. Can be the airbase name (passed as a string) or a Moose AIRBASE object. -- @return #RESCUEHELO self function RESCUEHELO:SetHomeBase(airbase) - self.airbase=airbase + if type(airbase)=="string" then + self.airbase=AIRBASE:FindByName(airbase) + else + self.airbase=airbase + end + if not self.airbase then + self:E(self.lid.."ERROR: Airbase is nil!") + end return self end @@ -755,10 +760,10 @@ function RESCUEHELO:onafterStart(From, Event, To) local delay=120 -- Set unique alias for spawn. - self.helogroupalias=string.format("%s_%s", self.helogroupname, self.carrier:GetName()) + local helogroupalias=string.format("%s_%s", self.helogroupname, self.carrier:GetName()) -- Spawn helo. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. - local Spawn=SPAWN:NewWithAlias(self.helogroupname, self.helogroupalias) + local Spawn=SPAWN:NewWithAlias(self.helogroupname, helogroupalias) -- Spawn in air or at airbase. if self.takeoff==SPAWN.Takeoff.Air then @@ -789,9 +794,6 @@ function RESCUEHELO:onafterStart(From, Event, To) -- Use an uncontrolled aircraft group. self.helo=GROUP:FindByName(self.helogroupname) - -- Also set the alias just in case. - self.helogroupalias=self.helogroupname - if self.helo:IsAlive() then -- Start uncontrolled group. diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 9a5cd29a9..ae30b28a3 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1912,8 +1912,7 @@ do -- Route methods --local PointAirbase=RTBAirbase:GetCoordinate():SetAltitude(coord.y):WaypointAirTurningPoint(nil ,Speed) -- Landing waypoint. More general than prev version since it should also work with FAPRS and ships. - local PointLanding=RTBAirbase:GetCoordinate():WaypointAirLanding(Speed, RTBAirbase) - + local PointLanding=RTBAirbase:GetCoordinate():WaypointAirLanding(Speed, RTBAirbase) -- Waypoint table. local Points={PointFrom, PointLanding} From cfc9c655c5e105079c06bbf9f0d2ddee96fdcc92 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 17 Dec 2018 19:39:23 +0100 Subject: [PATCH 105/147] Fixes for patrol. --- Moose Development/Moose/AI/AI_A2G_BAI.lua | 12 +- Moose Development/Moose/AI/AI_A2G_CAS.lua | 12 +- .../Moose/AI/AI_A2G_Dispatcher.lua | 14 +- Moose Development/Moose/AI/AI_A2G_Engage.lua | 2 +- Moose Development/Moose/AI/AI_A2G_Patrol.lua | 365 +++++------------- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 12 +- Moose Development/Moose/AI/AI_Air.lua | 2 +- 7 files changed, 138 insertions(+), 281 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index 5c91e4027..2806852a1 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -31,11 +31,19 @@ AI_A2G_BAI = { --- Creates a new AI_A2G_BAI object -- @param #AI_A2G_BAI self -- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO -- @return #AI_A2G_BAI -function AI_A2G_BAI:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) +function AI_A2G_BAI:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) ) -- #AI_A2G_BAI + local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_BAI return self end diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 4177b38ed..e2465d572 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -31,11 +31,19 @@ AI_A2G_CAS = { --- Creates a new AI_A2G_CAS object -- @param #AI_A2G_CAS self -- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO -- @return #AI_A2G_CAS -function AI_A2G_CAS:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) +function AI_A2G_CAS:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) ) -- #AI_A2G_CAS + local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_CAS return self end diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 13c6767d5..0070e3c6e 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -2761,7 +2761,7 @@ do -- AI_A2G_DISPATCHER -- Count the total of defenders on the battlefield. --local DefenderSize = Defender:GetInitialSize() if DefenderTask.Target then - --if DefenderTask.Fsm:Is( "Engaging" ) then + if DefenderTask.Fsm:Is( "Engaging" ) then self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) DefendersTotal = DefendersTotal + DefenderSize if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then @@ -2775,7 +2775,7 @@ do -- AI_A2G_DISPATCHER DefendersEngaged = 0 end end - --end + end end @@ -2921,7 +2921,9 @@ do -- AI_A2G_DISPATCHER if DefenderPatrol then - local Fsm = AI_A2G_PATROL:New( DefenderPatrol, Patrol.Zone, Patrol.FloorAltitude, Patrol.CeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.AltType ) + local AI_A2G_PATROL = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + + local Fsm = AI_A2G_PATROL[DefenseTaskType]:New( DefenderPatrol, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.Zone, Patrol.FloorAltitude, Patrol.CeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.AltType ) Fsm:SetDispatcher( self ) Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) @@ -2930,7 +2932,7 @@ do -- AI_A2G_DISPATCHER Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) Fsm:Start() - self:SetDefenderTask( SquadronName, DefenderPatrol, DefenseTaskType, Fsm ) + self:SetDefenderTask( SquadronName, DefenderPatrol, DefenseTaskType, Fsm, nil, DefenderGrouping ) function Fsm:onafterTakeoff( Defender, From, Event, To ) self:F({"Patrol Birth", Defender:GetName()}) @@ -2967,7 +2969,7 @@ do -- AI_A2G_DISPATCHER if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) Defender:Destroy() - self:ParkDefender( Squadron, Defender ) + Dispatcher:ParkDefender( Squadron, Defender ) end end end @@ -3425,7 +3427,7 @@ do -- AI_A2G_DISPATCHER if self.TacticalDisplay then -- Show tactical situation - Report:Add( string.format( "\n - %s %s ( %s ): ( #%d ) %s" , DetectedItem.Type or " --- ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) ) + Report:Add( string.format( "\n - %4s %s ( %s ): ( #%d ) %s" , DetectedItem.Type or " --- ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) ) for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do local Defender = Defender -- Wrapper.Group#GROUP if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then diff --git a/Moose Development/Moose/AI/AI_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua index a0513933f..7c09d1b56 100644 --- a/Moose Development/Moose/AI/AI_A2G_Engage.lua +++ b/Moose Development/Moose/AI/AI_A2G_Engage.lua @@ -97,7 +97,7 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) self.PatrolAltType = "RADIO" - self:AddTransition( { "Started", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + self:AddTransition( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. --- OnBefore Transition Handler for Event Engage. -- @function [parent=#AI_A2G_ENGAGE] OnBeforeEngage diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua index 9a667b5cc..185c4a02c 100644 --- a/Moose Development/Moose/AI/AI_A2G_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2G_Patrol.lua @@ -10,7 +10,7 @@ -- @image AI_Air_To_Ground_Patrol.JPG --- @type AI_A2G_PATROL --- @extends AI.AI_A2A_Patrol#AI_A2A_PATROL +-- @extends AI.AI_A2G_Engage#AI_A2G_ENGAGE --- The AI_A2G_PATROL class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} @@ -100,30 +100,33 @@ AI_A2G_PATROL = { --- Creates a new AI_A2G_PATROL object -- @param #AI_A2G_PATROL self -- @param Wrapper.Group#GROUP AIPatrol +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. -- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. --- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. --- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. -- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO -- @return #AI_A2G_PATROL -function AI_A2G_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, PatrolAltType ) +function AI_A2G_PATROL:New( AIPatrol, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2G:New( AIPatrol ) ) -- #AI_A2G_PATROL + local self = BASE:Inherit( self, AI_A2G_ENGAGE:New( AIPatrol, EngageMinSpeed, EngageMaxSpeed ) ) -- #AI_A2G_PATROL - self.Accomplished = false - self.Engaging = false + self.PatrolZone = PatrolZone + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed - self.EngageMinSpeed = EngageMinSpeed - self.EngageMaxSpeed = EngageMaxSpeed + -- defafult PatrolAltType to "RADIO" if not specified + self.PatrolAltType = PatrolAltType or "RADIO" - self:AddTransition( { "Patrolling", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + self:AddTransition( { "Started", "Airborne", "Refuelling" }, "Patrol", "Patrolling" ) - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_A2G_PATROL] OnBeforeEngage + --- OnBefore Transition Handler for Event Patrol. + -- @function [parent=#AI_A2G_PATROL] OnBeforePatrol -- @param #AI_A2G_PATROL self -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. -- @param #string From The From State string. @@ -131,44 +134,25 @@ function AI_A2G_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCei -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_A2G_PATROL] OnAfterEngage + --- OnAfter Transition Handler for Event Patrol. + -- @function [parent=#AI_A2G_PATROL] OnAfterPatrol -- @param #AI_A2G_PATROL self -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2G_PATROL] Engage + + --- Synchronous Event Trigger for Event Patrol. + -- @function [parent=#AI_A2G_PATROL] Patrol -- @param #AI_A2G_PATROL self - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2G_PATROL] __Engage + --- Asynchronous Event Trigger for Event Patrol. + -- @function [parent=#AI_A2G_PATROL] __Patrol -- @param #AI_A2G_PATROL self -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_A2G_PATROL] OnLeaveEngaging --- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_A2G_PATROL] OnEnterEngaging --- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_A2G_PATROL] OnBeforeFired + --- OnLeave Transition Handler for State Patrolling. + -- @function [parent=#AI_A2G_PATROL] OnLeavePatrolling -- @param #AI_A2G_PATROL self -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. -- @param #string From The From State string. @@ -176,27 +160,18 @@ function AI_A2G_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCei -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_A2G_PATROL] OnAfterFired + --- OnEnter Transition Handler for State Patrolling. + -- @function [parent=#AI_A2G_PATROL] OnEnterPatrolling -- @param #AI_A2G_PATROL self -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2G_PATROL] Fired - -- @param #AI_A2G_PATROL self - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2G_PATROL] __Fired - -- @param #AI_A2G_PATROL self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_A2G_PATROL] OnBeforeDestroy + self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + --- OnBefore Transition Handler for Event Route. + -- @function [parent=#AI_A2G_PATROL] OnBeforeRoute -- @param #AI_A2G_PATROL self -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. -- @param #string From The From State string. @@ -204,110 +179,30 @@ function AI_A2G_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCei -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_A2G_PATROL] OnAfterDestroy + --- OnAfter Transition Handler for Event Route. + -- @function [parent=#AI_A2G_PATROL] OnAfterRoute -- @param #AI_A2G_PATROL self -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2G_PATROL] Destroy + + --- Synchronous Event Trigger for Event Route. + -- @function [parent=#AI_A2G_PATROL] Route -- @param #AI_A2G_PATROL self - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2G_PATROL] __Destroy + --- Asynchronous Event Trigger for Event Route. + -- @function [parent=#AI_A2G_PATROL] __Route -- @param #AI_A2G_PATROL self -- @param #number Delay The delay in seconds. - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_A2G_PATROL] OnBeforeAbort - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_A2G_PATROL] OnAfterAbort - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2G_PATROL] Abort - -- @param #AI_A2G_PATROL self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2G_PATROL] __Abort - -- @param #AI_A2G_PATROL self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2G_PATROL] OnBeforeAccomplish - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2G_PATROL] OnAfterAccomplish - -- @param #AI_A2G_PATROL self - -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2G_PATROL] Accomplish - -- @param #AI_A2G_PATROL self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2G_PATROL] __Accomplish - -- @param #AI_A2G_PATROL self - -- @param #number Delay The delay in seconds. + self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. return self end ---- onafter State Transition for Event Patrol. --- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_PATROL:onafterStart( AIPatrol, From, Event, To ) - - AIPatrol:HandleEvent( EVENTS.Takeoff, nil, self ) - -end - ---- Set the Engage Zone which defines where the AI will engage bogies. --- @param #AI_A2G_PATROL self --- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. --- @return #AI_A2G_PATROL self -function AI_A2G_PATROL:SetEngageZone( EngageZone ) - self:F2() - - if EngageZone then - self.EngageZone = EngageZone - else - self.EngageZone = nil - end -end - --- Set the Engage Range when the AI will engage with airborne enemies. -- @param #AI_A2G_PATROL self -- @param #number EngageRange The Engage Range. @@ -322,157 +217,93 @@ function AI_A2G_PATROL:SetEngageRange( EngageRange ) end end ---- onafter State Transition for Event Patrol. +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. -- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The AI Group managed by the FSM. +-- @return #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. function AI_A2G_PATROL:onafterPatrol( AIPatrol, From, Event, To ) + self:F2() - -- Call the parent Start event handler - self:GetParent(self).onafterPatrol( self, AIPatrol, From, Event, To ) - self:HandleEvent( EVENTS.Dead ) + self:ClearTargetDistance() + self:__Route( 1 ) + + AIPatrol:OnReSpawn( + function( PatrolGroup ) + self:__Reset( 1 ) + self:__Route( 5 ) + end + ) end --- todo: need to fix this global function - --- @param Wrapper.Group#GROUP AIPatrol -function AI_A2G_PATROL.AttackRoute( AIPatrol, Fsm ) +-- This statis method is called from the route path within the last task at the last waaypoint of the AIPatrol. +-- Note that this method is required, as triggers the next route when patrolling for the AIPatrol. +function AI_A2G_PATROL.PatrolRoute( AIPatrol, Fsm ) - AIPatrol:F( { "AI_A2G_PATROL.AttackRoute:", AIPatrol:GetName() } ) + AIPatrol:F( { "AI_A2G_PATROL.PatrolRoute:", AIPatrol:GetName() } ) if AIPatrol:IsAlive() then - Fsm:__Engage( 0.5 ) + Fsm:Route() end + end ---- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. -function AI_A2G_PATROL:onbeforeEngage( AIPatrol, From, Event, To ) - - if self.Accomplished == true then - return false +function AI_A2G_PATROL:onafterRoute( AIPatrol, From, Event, To ) + + self:F2() + + -- When RTB, don't allow anymore the routing. + if From == "RTB" then + return end -end ---- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_PATROL:onafterAbort( AIPatrol, From, Event, To ) - AIPatrol:ClearTasks() - self:__Route( 0.5 ) -end - - ---- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The AIPatrol Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_PATROL:onafterEngage( AIPatrol, From, Event, To, AttackSetUnit ) - - self:F( { AIPatrol, From, Event, To, AttackSetUnit} ) - - self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT - local FirstAttackUnit = self.AttackSetUnit:GetFirst() -- Wrapper.Unit#UNIT - - if FirstAttackUnit and FirstAttackUnit:IsAlive() then -- If there is no attacker anymore, stop the engagement. - - if AIPatrol:IsAlive() then + if AIPatrol:IsAlive() then + + local PatrolRoute = {} - local EngageRoute = {} + --- Calculate the target route point. + + local CurrentCoord = AIPatrol:GetCoordinate() + + local ToTargetCoord = self.PatrolZone:GetRandomPointVec2() + ToTargetCoord:SetAlt( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) ) + self:SetTargetDistance( ToTargetCoord ) -- For RTB status check + + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + + --- Create a route point of type air. + local ToPatrolRoutePoint = ToTargetCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) - --- Calculate the target route point. - local CurrentCoord = AIPatrol:GetCoordinate() - local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - local ToInterceptAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = CurrentCoord:Translate( 5000, ToInterceptAngle ):WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = ToInterceptAngle, ToTargetSpeed = ToTargetSpeed } ) - self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint + PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint + PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint + + local Tasks = {} + Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_A2G_PATROL.PatrolRoute", self ) + PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) + + AIPatrol:OptionROEReturnFire() + AIPatrol:OptionROTEvadeFire() - local AttackTasks = {} - - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT - self:T( { "Attacking Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - AttackTasks[#AttackTasks+1] = AIPatrol:TaskAttackUnit( AttackUnit ) - end - end - - if #AttackTasks == 0 then - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 0.5 ) - else - AIPatrol:OptionROEOpenFire() - AIPatrol:OptionROTEvadeFire() - - AttackTasks[#AttackTasks+1] = AIPatrol:TaskFunction( "AI_A2G_PATROL.AttackRoute", self ) - EngageRoute[#EngageRoute].task = AIPatrol:TaskCombo( AttackTasks ) - end - - AIPatrol:Route( EngageRoute, 0.5 ) - end - else - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 0.5 ) + AIPatrol:Route( PatrolRoute, 0.5 ) end -end ---- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2G_PATROL:onafterAccomplish( AIPatrol, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -end - ---- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_A2G_PATROL:onafterDestroy( AIPatrol, From, Event, To, EventData ) - - if EventData.IniUnit then - self.AttackUnits[EventData.IniUnit] = nil - end -end - ---- @param #AI_A2G_PATROL self --- @param Core.Event#EVENTDATA EventData -function AI_A2G_PATROL:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end end --- @param Wrapper.Group#GROUP AIPatrol diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index c0db300d4..dfaf89b34 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -81,11 +81,19 @@ AI_A2G_SEAD = { --- Creates a new AI_A2G_SEAD object -- @param #AI_A2G_SEAD self -- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO -- @return #AI_A2G_SEAD -function AI_A2G_SEAD:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) +function AI_A2G_SEAD:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) ) -- #AI_A2G_SEAD + local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_SEAD return self end diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 85d125384..3c808beaa 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -511,7 +511,7 @@ function AI_AIR:onafterStatus() end if not self:Is("Home") then - self:__Status( 10 ) + self:__Status( 30 ) end end From 6c4bde4ceb294ddbba535b20444e7725e31bbf69 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 17 Dec 2018 20:59:58 +0100 Subject: [PATCH 106/147] Fixes --- Moose Development/Moose/AI/AI_A2G.lua | 2 +- Moose Development/Moose/AI/AI_A2G_CAS.lua | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G.lua b/Moose Development/Moose/AI/AI_A2G.lua index 2f6a8f500..b80eb8796 100644 --- a/Moose Development/Moose/AI/AI_A2G.lua +++ b/Moose Development/Moose/AI/AI_A2G.lua @@ -61,7 +61,7 @@ function AI_A2G:New( AIGroup ) local self = BASE:Inherit( self, AI_AIR:New( AIGroup ) ) -- #AI_A2G self:SetFuelThreshold( .2, 60 ) - self:SetDamageThreshold( 0.4 ) + self:SetDamageThreshold( 0.95 ) self:SetDisengageRadius( 70000 ) return self diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index e2465d572..e3ef10419 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -98,8 +98,10 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) + --- Create a route point of type air. - local ToWP = ToCoord:Translate( 10000, FromEngageAngle ):WaypointAir( + local ToWP = ToCoord:Translate( EngageDistance, FromEngageAngle ):WaypointAir( self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, From 75ac76a8e5c664995982ed903bd3a52537b8bef7 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 18 Dec 2018 14:04:31 +0100 Subject: [PATCH 107/147] AIBOSS v0.5.6 RESCUEHELO v0.9.9 --- Moose Development/Moose/Ops/Airboss.lua | 815 +++++++++++------- .../Moose/Ops/RecoveryTanker.lua | 4 +- Moose Development/Moose/Ops/RescueHelo.lua | 36 +- Moose Development/Moose/Utilities/Utils.lua | 6 +- 4 files changed, 525 insertions(+), 336 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 6cf938fe6..86834c16b 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1,4 +1,4 @@ ---- **Ops** - (R2.5) - Manages aircraft operations on carriers. +--- **Ops** - (R2.5) - Manages aircraft recoveries for carrier operations. -- -- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers. -- @@ -14,40 +14,52 @@ -- * Voice over support for LSO and Marshal radio transmissions. -- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, -- help function (player aircraft attitude, marking of pattern zones etc). --- * Recovery tanker and refueling option via integration of @{#Ops.RecoveryTanker} class. --- * Rescue helo option via @{#Ops.RescueHelo} class. +-- * Recovery tanker and refueling option via integration of @{Ops.RecoveryTanker} class. +-- * Rescue helicopter option via @{Ops.RescueHelo} class. -- * Many parameters customizable by convenient user API functions. -- * Multiple carrier support due to object oriented approach. +-- * Unlimited number of players. -- * Finite State Machine (FSM) implementation. -- --- Supported Carriers: +-- **Supported Carriers:** -- --- * USS John C. Stennis +-- * [USS John C. Stennis](https://en.wikipedia.org/wiki/USS_John_C._Stennis) (CVN-74) -- --- Supported Player and AI Aircraft: +-- **Supported Aircraft:** -- -- * F/A-18C Hornet Lot 20 (player+AI) --- * A-4E-C Skyhawk Community Mod (player+AI) +-- * A-4E Skyhawk Community Mod (player+AI) -- * F/A-18C Hornet (AI) -- * F-14A Tomcat (AI) -- * E-2D Hawkeye (AI) -- * S-3B Viking & tanker version (AI) -- -- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) as aircraft and the USS John C. Stennis as carrier. --- The community A-4E mod is also supported in priciple but may needs further tweaking of parameters such as on speed AoA values. +-- The A-4E community mod is also supported in priciple but may need further tweaking of parameters. -- --- The implemenation is kept very general. So other including other aircraft and carriers in future is possible. (*Winter is coming!*) +-- The implemenation is kept very general. So other including other aircraft and carriers in future is possible. [*Winter is coming!*](https://forums.eagle.ru/forumdisplay.php?f=395) -- But each aircraft or carrier needs a different set of optimized individual parameters. -- --- **PLEASE NOTE** that his class is work in progress and in an **alpha** stage and very much **work in progress**. --- Your constructive feedback is both necessary and highly appreciated. +-- **PLEASE NOTE** that his class is work in progress and in an early **alpha** stage. Many/most things work already very nicely but there a lot of cases I did not run into yet. +-- Therefore, your *constructive* feedback is both necessary and appreciated! +-- +-- ### Open Questions? +-- +-- * What are the conditions for a foul deck wave off? +-- * What is the next step after a pattern wave off during Case II or III recovery? +-- * What is the condition for a "fly through" \\ or \/ LSO grade? +-- * The above question is one of many regarding LSO grade. If you have more info, please share. +-- +-- If you know the answer to any of this, please get in touch with me! +-- The necessary infrastructure to implement it is most likely already there, but I was not 100% sure about the exact conditions. -- -- === -- -- ### Author: **funkyfranky** --- ### Special thanks to --- **Bankler** for his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script! --- His work was the initial inspiration for this class. Also note that this class uses some routines for determining the player position in Case I recoveries developed by Bankler. +-- ### Special Thanks To **Bankler** +-- For his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script! +-- His work was the initial inspiration for this class. Also note that this implementation uses some routines for determining the player position in Case I recoveries he developed. +-- Bankler was kind enough to allow me to add this to the class - thanks again! -- -- @module Ops.Airboss -- @image MOOSE.JPG @@ -115,11 +127,11 @@ -- @field #string defaultskill Default player skill @{#AIRBOSS.Difficulty}. -- @extends Core.Fsm#FSM ---- The boss! +--- Be the boss! -- -- === -- --- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Main.jpg) +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Main.png) -- -- # The AIRBOSS Concept -- @@ -130,14 +142,14 @@ -- The AIRBOSS class supports all three commonly used recovery cases, i.e. -- -- * **CASE I** during daytime and good weather, --- * **CASE II** during daytime with poor visibility conditions, +-- * **CASE II** during daytime but poor visibility conditions, -- * **CASE III** during nighttime recoveries. -- -- That being said, this script allows you to use any of the three cases to be used at any time. Or, in other words, *you* need to specify when which case is safe and appropriate. -- -- This is a lot of responsability. *You* are the boss, but *you* need to make the right decisions or things will go terribly wrong! -- --- Recovery windows can be set up via the @{#AIRBOSS.AddRecoveryWindow} function as explained below. With this it is possible to seamlessly switch recovery cases even in the same mission. +-- Recovery windows can be set up via the @{#AIRBOSS.AddRecoveryWindow} function as explained below. With this it is possible to seamlessly (within reason!) switch recovery cases in the same mission. -- -- ## CASE I -- @@ -196,9 +208,9 @@ -- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") -- airbossStennis:Start() -- --- The first line creates and AIRBOSS object via the @{#AIRBOSS.New}(*carriername*, *alias*) constructor. The first parameter *carriername* is name of the carrier unit as +-- The **first line** creates and AIRBOSS object via the @{#AIRBOSS.New}(*carriername*, *alias*) constructor. The first parameter *carriername* is name of the carrier unit as -- defined in the mission editor. The second parameter *alias* is optional. This name will, e.g., be used for the F10 radio menu entry. If not given, the alias is identical --- to the carriername of the first parameter. +-- to the *carriername* of the first parameter. -- -- This simple script initializes a lot of parameters with default values: -- @@ -207,6 +219,8 @@ -- * LSO radio is set to 264 MHz FM, see @{#AIRBOSS.SetLSORadio} -- * Marshal radio is set to 305 MHz FM, see @{#AIRBOSS.SetMarshalRadio} -- * Default recovery case is set to 1, see @{#AIRBOSS.SetRecoveryCase} +-- +-- The **second line** starts the AIRBOSS class. If you set options this should happen after the @{#AIRBOSS.New} and before @{#AIRBOSS.Start} command. -- -- ## Recovery Windows -- @@ -289,8 +303,8 @@ -- -- These commands can be used to mark marshal or landing pattern zones. -- --- * **Smoke My Marshal Zone** This smokes the the surrounding area of the currently assigned marshal zone of the player. Player has to be registered for marshal. --- * **Flare My Marshal Zone** Similar to smoke but uses flares to mark the marshal zone. +-- * **Smoke My Marshal Zone** This smokes the surrounding area of the currently assigned Marshal zone of the player. Player has to be registered in Marshal queue. +-- * **Flare My Marshal Zone** Similar to smoke but uses flares to mark the Marshal zone. -- * **Smoke Pattern Zones** Smoke is used to mark the landing pattern zone of the player depending on his recovery case. -- For Case I this is the initial zone. For Case II/III and three these are the Platform, Arc turn, Dirty Up, Bullseye/Initial zones as well as the approach corridor. -- * **Flare Pattern Zones** Similar to smoke but uses flares to mark the pattern zones. @@ -365,9 +379,14 @@ -- -- Grading at each step includes the above calls, i.e. -- --- * Linup: (LUL), LUL, _LUL_, (RUL), RUL, \_RUL\_ --- * Alitude: (H), H, _H_, (L), L, \_L\_ --- * Speed: (F), F, _F_, (SLO), SLO, \_SLO\_ +-- * **L**ined **U**p **L**eft or **R**ight: LUL, LUR +-- * Too **H**igh or too **L**ow: H, L +-- * Too **F**ast or too **SLO**w: F, SLO +-- +-- Each grading, x, is subdivided by +-- +-- * (x): parenthesis, indicating "a little" for a minor deviation and +-- * \_x\_: underline, indicating "a lot" for major deviations. -- -- The position at the landing event is analyzed and the corresponding trapped wire calculated. If no wire was caught, the LSO will give the bolter call. -- @@ -390,9 +409,9 @@ -- -- # AI Handling -- --- The AIRBOSS class allows to handle incoming AI units and integrate them into the marshal and landing pattern. +-- The @{#AIRBOSS} class allows to handle incoming AI units and integrate them into the marshal and landing pattern. -- --- By default, incoming carrier capable aircraft which are detecting inside the CCZ and approach the carrier by more than 10 NM are automatically guided to the holding zone. +-- By default, incoming carrier capable aircraft which are detecting inside the CCZ and approach the carrier by more than 5 NM are automatically guided to the holding zone. -- Each AI group gets its own marshal stack in the holding pattern. Once a recovery window opens, the AI group of the lowest stack is transitioning to the landing pattern -- and the Marshal stack collapses. -- @@ -402,7 +421,7 @@ -- -- The holding position of the AI is updated regularly when the carrier has changed its position by more then 2.5 NM or changed its course significantly. -- The patterns are realized by orbit or racetrack patterns of the DCS scripting API. --- However, when the position is updated or the marshal stack collapses, it comes to disruptions of the regular orbit becase a new waypoint with a new +-- However, when the position is updated or the marshal stack collapses, it comes to disruptions of the regular orbit because a new waypoint with a new -- orbit task needs to be created. -- -- # Debugging @@ -1019,13 +1038,14 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.5" +AIRBOSS.version="0.5.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: +-- TODO: Carrier zone with dimensions of carrier. to check if landing happend on deck. +-- TODO: Carrier runway zone for fould deck check. -- TODO: Subtitles off options on player level. -- TODO: PWO during case 2/3. Also when too close to other player. -- TODO: Option to filter AI groups for recovery. @@ -1771,7 +1791,7 @@ function AIRBOSS:_CheckAIStatus() self:MessageToPattern(text, element.onboard, "", 3, false, 0, true) -- Debug message. - MESSAGE:New(string.format("%s, %s", element.onboard..text), 15, "DEBUG"):ToAllIf(self.Debug) + MESSAGE:New(string.format("%s, %s", element.onboard, text), 15, "DEBUG"):ToAllIf(self.Debug) -- Paddles: Roger ball after 3 seconds. self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 3) @@ -2109,17 +2129,14 @@ end function AIRBOSS._ReachedHoldingZone(group, airboss, flight) -- Debug message. - local text=string.format("Group %s has reached the holding zone.", group:GetName()) - - -- Debug mark. - if airboss.Debug and false then - local pos=group:GetCoordinate() - local MarkerID=pos:MarkToAll(string.format("Flight group %s reached holding zone.", group:GetName())) - end - - -- Message output + local text=string.format("Flight %s reached holding zone.", group:GetName()) MESSAGE:New(text,10):ToAllIf(airboss.Debug) - airboss:T(airboss.lid..text) + airboss:I(airboss.lid..text) + + -- Debug mark. + if airboss.Debug then + group:GetCoordinate():MarkToAll(text) + end -- Set holding flag true and set timestamp for marshal time check. if flight then @@ -2602,7 +2619,7 @@ end function AIRBOSS:_GetNextMarshalFight() -- Min 5 min in marshal before send to landing pattern. - local TmarshalMin=5*60 + local TmarshalMin=10*60 for _,_flight in pairs(self.Qmarshal) do local flight=_flight --#AIRBOSS.FlightGroup @@ -2635,9 +2652,7 @@ function AIRBOSS:_CheckQueue() -- Get number of aircraft units(!) currently in pattern. local _,npattern=self:_GetQueueInfo(self.Qpattern) - -- Get number of flight groups(!) in marshal pattern. - local nmarshal,_=self:_GetQueueInfo(self.Qmarshal) - + -- Get next marshal flight. local marshalflight=self:_GetNextMarshalFight() -- Check if there are flights in marshal strack and if the pattern is free. @@ -2672,7 +2687,7 @@ function AIRBOSS:_CheckQueue() if pcase==1 then TpatternMin=3*60*npunits --45*npunits -- 45 seconds interval per plane! else - TpatternMin=6*60*npunits --120*npunits -- 120 seconds interval per plane! + TpatternMin=3*60*npunits --120*npunits -- 120 seconds interval per plane! end -- Check recovery window open and enough space to last pattern flight. @@ -2786,6 +2801,7 @@ function AIRBOSS:_ScanCarrierZone() for _,_flight in pairs(self.flights) do local flight=_flight --#AIRBOSS.FlightGroup if insideCCA[flight.groupname]==nil then + -- TODO: do not remove flights in marshal pattern. At least for case 3. if zone is set small, they might get out! table.insert(remove, flight.group) end end @@ -2837,20 +2853,21 @@ function AIRBOSS:_MarshalPlayer(playerData) end ---- Tell AI to orbit at a specified position at a specified alititude with a specified speed. +--- Command AI flight to orbit at a specified position at a specified alititude with a specified speed. +-- If the flight is not already holding in the Marshal stack, it is guided there first. -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight group. --- @param #number nstack Stack number of group. (Should be #self.Qmarshal+1 for new flight groups.) +-- @param #number nstack Stack number of group. This should be #self.Qmarshal+1 for new flight groups. function AIRBOSS:_MarshalAI(flight, nstack) -- Flight group name. local group=flight.group local groupname=flight.groupname - - -- Debug info. - self:I(self.lid..string.format("Sending AI group %s to marshal stack %d. Current stack/flag value=%d.", groupname, nstack, flight.flag:Get())) - -- Set flag/stack value. + -- Get old/current stack. + local ostack=flight.flag:Get() + + -- Set new stack. flight.flag:Set(nstack) -- Current carrier position. @@ -2858,108 +2875,114 @@ function AIRBOSS:_MarshalAI(flight, nstack) -- Carrier heading. local hdg=self:GetHeading() + + -- Recovery case. + local case=flight.case - -- Aircraft speed 272 knots when orbiting the pattern. (Orbit expects m/s.) - local SpeedOrbit=UTILS.KnotsToMps(272) + -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) + local speedOrbitMps=UTILS.KnotsToMps(274) + + -- Orbit speed in km/h for waypoints. + local speedOrbitKmh=UTILS.KnotsToKmph(274) -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) - local SpeedTransit=UTILS.KnotsToKmph(400) + local speedTransit=UTILS.KnotsToKmph(400) - --- Create a DCS task to orbit at a certain altitude. - local function _taskorbit(p1, alt, speed, stopflag, p2) - local DCSTask={} - DCSTask.id="ControlledTask" - DCSTask.params={} - DCSTask.params.task=group:TaskOrbit(p1, alt, speed, p2) - DCSTask.params.stopCondition={userFlag=groupname, userFlagValue=stopflag} - return DCSTask - end - - -- Waypoints array. + local altitude + local p0 --Core.Point#COORDINATE + local p1 --Core.Point#COORDINATE + local p2 --Core.Point#COORDINATE + + -- Get altitude and positions. + altitude, p1, p2=self:_GetMarshalAltitude(nstack, case) + + -- Waypoints array to be filled depending on case etc. local wp={} - - -- Current position. Not sure if necessary but might be. Need to test if it hurts or not. - wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, SpeedTransit, {}, "Current Position") - + -- If flight has not arrived in the holding zone, we guide it there. if not flight.holding then - - -- Get altitude and positions. - local Altitude, p1, p2=self:_GetMarshalAltitude(nstack, flight.case) + + ---------------------- + -- Route to Holding -- + ---------------------- + + -- Debug info. + self:I(self.lid..string.format("Guiding AI flight %s to marshal stack %d-->%d.", groupname, ostack, nstack)) + -- Current position. Always good for as the first waypoint. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") + -- Task function when arriving at the holding zone. This will set flight.holding=true. local TaskArrivedHolding=flight.group:TaskFunction("AIRBOSS._ReachedHoldingZone", self, flight) - if flight.case==1 then - -- Waypoint "north" of carrier's holding zone. - --wp[2]=p1:Translate(UTILS.NMToMeters(10), hdg):WaypointAirTurningPoint(nil, SpeedTransit, {}, "Prepare Entering Case I Marshal Pattern") - -- Enter pattern from "north" to "south". - wp[2]=Carrier:Translate(UTILS.NMToMeters(10), hdg-30):WaypointAirTurningPoint(nil, SpeedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") - else + -- Select case. + if case==1 then + + -- Initial point 7 NM and a bit port of carrier. -- TODO: Test and tune! - wp[2]=p1:WaypointAirTurningPoint(nil, SpeedTransit, {TaskArrivedHolding}, "Entering Marshal Pattern") - end - - end - - -- Set up waypoints including collapsing the stack. - for stack=nstack, 1, -1 do - - -- TODO: skip stack 6 if recoverytanker (or at whatever angels the tanker orbits). - - -- Get altitude and positions. - local Altitude, p1, p2=self:_GetMarshalAltitude(stack, flight.case) - - -- Correct CCW pattern for CASE II/III. - local c1=nil --Core.Point#COORDINATE - local c2=nil --Core.Point#COORDINATE - local p0=nil --Core.Point#COORDINATE - if flight.case==1 then - c1=p1 - c2=p2 - p0=p1 --self:GetCoordinate():Translate(UTILS.NMToMeters(5), -90):SetAltitude(Altitude) - p0=self:GetCoordinate():Translate(UTILS.NMToMeters(2.5/math.sqrt(2)), 225):SetAltitude(Altitude) - --p0=self:GetCoordinate():Translate(UTILS.NMToMeters(2), hdg+190):SetAltitude(Altitude) - else - c1=p2 - c2=p1 - p0=c2 - end - - -- Distance to the boat. - local Dist=p1:Get2DDistance(self:GetCoordinate()) - - -- Task: orbit at specified position, altitude and speed until flag=stack-1 - local TaskOrbit=_taskorbit(c1, Altitude, SpeedOrbit, stack-1, c2) - - -- Waypoint description. - local text=string.format("Flight %s: Marshal stack %d: alt=%d, dist=%.1f, speed=%d", flight.groupname, stack, UTILS.MetersToFeet(Altitude), UTILS.MetersToNM(Dist), UTILS.MpsToKnots(SpeedOrbit)) - - -- Debug mark. - if self.Debug or true then - --c1:MarkToAll(text) - if c2 then - --c2:MarkToAll(text) - end - end - p0:MarkToAll("p0") - p1:MarkToAll("p1") - p2:MarkToAll("p2") - - -- Waypoint. - -- TODO: p0? - wp[#wp+1]=p0:WaypointAirTurningPoint(nil, SpeedTransit, {TaskOrbit}, text) - - end - - -- Landing waypoint. (Done separately now). - --wp[#wp+1]=Carrier:SetAltitude(250):WaypointAirLanding(Speed, self.airbase, nil, "Landing") + local pE=Carrier:SetAltitude(altitude):Translate(UTILS.NMToMeters(7), hdg-30) + + -- Entry point 5 NM port and slightly astern the boat. + p0=Carrier:SetAltitude(altitude):Translate(UTILS.NMToMeters(5*math.sqrt(2)), hdg-135) + + -- Waypoint ahead of carrier's holding zone. + wp[#wp+1]=pE:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") + else + + -- Get correct radial depending on recovery case including offset. + local radial + if case==2 then + radial=self:GetRadialCase2(false, true) + elseif case==3 then + radial=self:GetRadialCase3(false, true) + end + + -- Point in the middle of the race track and a 5 NM more port perpendicular. + p0=p2:Translate(UTILS.NMToMeters(5), radial+90):Translate(UTILS.NMToMeters(5), radial) + + -- Entering Case II/III marshal pattern waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case II/III Marshal Pattern") + + end + + else + + ------------------------ + -- In Marshal Pattern -- + ------------------------ + + -- Debug info. + self:I(self.lid..string.format("Updating AI flight %s at marshal stack %d-->%d.", groupname, ostack, nstack)) + + -- Current position. Speed expected in km/h. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedOrbitKmh, {}, "Current Position") + + -- Create new waypoint 1 Nm ahead of current positon. + -- TODO: Set altitude here or take the one of the orbit task? Maybe depends on ostack>nstack or ostack==nstack. + p0=group:GetCoordinate():Translate(UTILS.NMToMeters(1), group:GetHeading()) + + end + + -- Set orbit task. + local taskorbit=group:TaskOrbit(p1, altitude, speedOrbitMps, p2) + + -- Orbit at waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedOrbitKmh, {taskorbit}, string.format("Marshal Orbit Stack %d", nstack)) + + -- Debug markers. + if self.Debug then + p0:MarkToAll("WP P0 "..groupname) + p1:MarkToAll("RT P1 "..groupname) + p2:MarkToAll("RT P2 "..groupname) + end + -- Reinit waypoints. group:WayPointInitialize(wp) -- Route group. group:Route(wp, 0) + end --- Tell AI to land on the carrier. @@ -2967,15 +2990,22 @@ end -- @param #AIRBOSS.FlightGroup flight Flight group. function AIRBOSS:_LandAI(flight) + -- Debug info. + self:I(self.lid..string.format("Landing AI flight %s.", flight.groupname)) + -- Aircraft speed when flying the pattern. - local Speed=UTILS.KnotsToKmph(272) + local Speed=UTILS.KnotsToKmph(274) + -- Carrier position. local Carrier=self:GetCoordinate() + + -- Carrier heading. local hdg=self:GetHeading() -- Waypoints array. local wp={} + -- Current positon. wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, Speed, {}, "Current position") -- Landing waypoint 5 NM behind carrier at 250 ASL. @@ -2988,13 +3018,13 @@ function AIRBOSS:_LandAI(flight) flight.group:Route(wp, 0) end ---- Get marshal altitude and position. +--- Get marshal altitude and two positions of a counter-clockwise race track pattern. -- @param #AIRBOSS self -- @param #number stack Assigned stack number. Counting starts at one, i.e. stack=1 is the first stack. -- @param #number case Recovery case. Default is self.case. -- @return #number Holding altitude in meters. --- @return Core.Point#COORDINATE Holding position coordinate. --- @return Core.Point#COORDINATE Second holding position coordinate of racetrack pattern for CASE II/III recoveries. +-- @return Core.Point#COORDINATE First race track coordinate. +-- @return Core.Point#COORDINATE Second race track coordinate. function AIRBOSS:_GetMarshalAltitude(stack, case) -- Stack <= 0. @@ -3015,21 +3045,23 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) local p2=nil --Core.Point#COORDINATE if case==1 then + -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next stack. angels0=2 - -- Distance 2.5 NM. - Dist=UTILS.NMToMeters(2.5*math.sqrt(2)) - -- Get true heading of carrier. local hdg=self.carrier:GetHeading() - -- Center of holding pattern point. We give it a little head start -70 instead of -90 degrees. - p1=Carrier:Translate(Dist, hdg-45) + -- For CCW pattern: First point astern, second ahead of the carrier. + + -- First point 1 NM astern. + p1=Carrier --:Translate(-UTILS.NMToMeters(1.0), hdg) + + -- Seconds point 2 NM ahead. + p2=Carrier:Translate( UTILS.NMToMeters(1), hdg) - p1=Carrier:Translate(UTILS.NMToMeters(1.0), hdg) - p2=Carrier:Translate(UTILS.NMToMeters(3.5), hdg) else + -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. angels0=6 @@ -3044,12 +3076,15 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) radial=self:GetRadialCase3(false, true) end + -- For CCW pattern: p1 further astern than p2. + -- First point of race track pattern - p1=Carrier:Translate(Dist, radial) + p1=Carrier:Translate(Dist+UTILS.NMToMeters(10), radial) -- Second point which is 10 NM further behind. --TODO: check if 10 NM is okay. - p2=Carrier:Translate(Dist+UTILS.NMToMeters(10), radial) + p2=Carrier:Translate(Dist, radial) + end -- Pattern altitude. @@ -3057,9 +3092,7 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) -- Set altitude of coordinate. p1:SetAltitude(altitude, true) - if p2 then - p2:SetAltitude(altitude, true) - end + p2:SetAltitude(altitude, true) return altitude, p1, p2 end @@ -3156,11 +3189,23 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- TODO: If we include the recovery tanker, this needs to be generalized. mflight.flag:Set(mstack-1) - -- Inform players. - if mflight.ai==false and mflight.difficulty~=AIRBOSS.Difficulty.HARD then - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(mstack-1, case)) - local text=string.format("descent to next lower stack at %d ft", alt) - self:MessageToPlayer(mflight, text, "MARSHAL") + if mflight.ai then + + -- Command AI to decrease stack. + self:_MarshalAI(flight, mstack-1) + + else + + -- Inform players. + if mflight.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Send message to all non-pros that they can descent. + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(mstack-1, case)) + local text=string.format("descent to next lower stack at %d ft", alt) + self:MessageToPlayer(mflight, text, "MARSHAL") + + end + end -- Debug info. @@ -3491,11 +3536,12 @@ end --- Initialize player data by (re-)setting parmeters to initial values. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step (Optional) New player step. Default UNDEFINED. -- @return #AIRBOSS.PlayerData Initialized player data. -function AIRBOSS:_InitPlayer(playerData) +function AIRBOSS:_InitPlayer(playerData, step) self:I(self.lid..string.format("Initializing player data for %s callsign %s.", playerData.name, playerData.callsign)) - playerData.step=AIRBOSS.PatternStep.UNDEFINED + playerData.step=step or AIRBOSS.PatternStep.UNDEFINED playerData.groove={} playerData.debrief={} playerData.warning=nil @@ -3808,6 +3854,14 @@ function AIRBOSS:_CheckPatternUpdate() end + -- Inform player about new final bearing. + if Dchange then + -- 99, new final bearing XXX + local FB=self:GetFinalBearing(true) + local text=string.format("new final bearing %d.", FB) + self:MessageToAll(text, "MARSHAL", "99", 10) + end + -- Reset parameters for next update check. self.Corientation=vNew self.Cposition=pos @@ -3930,7 +3984,7 @@ function AIRBOSS:_CheckPlayerStatus() -- CASE I/II: Abeam position. self:_Abeam(playerData) - + elseif playerData.step==AIRBOSS.PatternStep.NINETY then -- CASE:I/II: Check long down wind leg. @@ -3962,7 +4016,7 @@ function AIRBOSS:_CheckPlayerStatus() elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then -- Debriefing in 10 seconds. - SCHEDULER:New(nil, self._Debrief, {self, playerData}, 10) + SCHEDULER:New(self, self._Debrief, {playerData}, 10) -- Undefined status. playerData.step=AIRBOSS.PatternStep.UNDEFINED @@ -4054,24 +4108,25 @@ function AIRBOSS:OnEventLand(EventData) 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)) - - -- Check if player or AI landed. - if _unit and _playername then - -- Human Player landed. - local _uid=_unit:GetID() - local _group=_unit:GetGroup() - local _callsign=_unit:GetCallsign() - - -- This would be the closest airbase. - local airbase=EventData.Place - local airbasename=tostring(airbase:GetName()) - - -- TODO: also check distance to airbase since landing "in the water" also trigger a landing event! - - -- Check if player landed on the right airbase. - if airbasename==self.airbase:GetName() then + -- This would be the closest airbase. + local airbase=EventData.Place + local airbasename=tostring(airbase:GetName()) + + -- Check if aircraft landed on the right airbase. + if airbasename==self.airbase:GetName() then + + -- Check if player or AI landed. + if _unit and _playername then + -- Human Player landed. + -- Get info. + local _uid=_unit:GetID() + local _group=_unit:GetGroup() + local _callsign=_unit:GetCallsign() + + -- TODO: also check distance to airbase since landing "in the water" also trigger a landing event! + -- Debug output. local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename) self:I(self.lid..text) @@ -4080,7 +4135,7 @@ function AIRBOSS:OnEventLand(EventData) -- Player data. local playerData=self.players[_playername] --#AIRBOSS.PlayerData - -- Coordinate at landing event + -- Coordinate at landing event. local coord=playerData.unit:GetCoordinate() -- Get distances relative to @@ -4095,7 +4150,7 @@ function AIRBOSS:OnEventLand(EventData) end -- Debug output - if self.Debug then + if self.Debug and false then local hdg=self.carrier:GetHeading()+self.carrierparam.rwyangle -- Debug marks of wires. @@ -4139,32 +4194,33 @@ function AIRBOSS:OnEventLand(EventData) playerData.step=AIRBOSS.PatternStep.UNDEFINED -- Call trapped function in 3 seconds to make sure we did not bolter. - SCHEDULER:New(nil, self._Trapped,{self, playerData}, 3) + SCHEDULER:New(self, self._Trapped, {playerData}, 3) + + else + + -- AI unit landed. + + -- Coordinate at landing event + local coord=EventData.IniUnit:GetCoordinate() + + -- Debug mark of player landing coord. + local dist=coord:Get2DDistance(self:GetCoordinate()) + + -- Get wire + local wire=self:_GetWire(self:GetCoordinate(), coord, 0) + + -- Aircraft type. + local _type=EventData.IniUnit:GetTypeName() + + -- Debug text. + local text=string.format("AI %s of type %s landed at dist=%.1f m. Trapped wire=%d.", EventData.IniUnitName, _type, dist, wire) + self:I(self.lid..text) + + -- AI always lands ==> remove unit from flight group and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) + end - - else - -- AI unit landed. - - -- Coordinate at landing event - local coord=EventData.IniUnit:GetCoordinate() - - -- Debug mark of player landing coord. - local dist=coord:Get2DDistance(self:GetCoordinate()) - - -- Get wire - local wire=self:_GetWire(self:GetCoordinate(), coord, 0) - - -- Aircraft type. - local _type=EventData.IniUnit:GetTypeName() - - -- Debug text. - local text=string.format("AI %s of type %s landed at dist=%.1f m. Trapped wire=%d.", EventData.IniUnitName, _type, dist, wire) - self:I(self.lid..text) - - -- AI always lands ==> remove unit from flight group and queues. - self:_RemoveUnitFromFlight(EventData.IniUnit) - end - + end end --- Airboss event handler for event crash. @@ -4312,7 +4368,11 @@ function AIRBOSS:_Holding(playerData) end ---- Commence approach. +--- Commence approach. This step initializes the player data. Next step depends on recovery case: +-- +-- * Case 1: Initial +-- * Case 2/3: Platform +-- -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. function AIRBOSS:_Commencing(playerData) @@ -4324,7 +4384,7 @@ function AIRBOSS:_Commencing(playerData) local text=string.format("Commencing. (Case %d)", playerData.case) -- Message to all players. - self:MessageToAll(text, playerData.onboard, "", 5) + self:MessageToMarshal(text, playerData.onboard, "", 5) -- Next step: depends on case recovery. if playerData.case==1 then @@ -4334,6 +4394,10 @@ function AIRBOSS:_Commencing(playerData) -- CASE II/III: Player has to start the descent at 4000 ft/min to the platform at 5k ft. playerData.step=AIRBOSS.PatternStep.PLATFORM end + + -- Next step hint. + self:_StepHint(playerData) + playerData.warning=nil end --- Start pattern when player enters the initial zone in case I/II recoveries. @@ -4347,8 +4411,8 @@ function AIRBOSS:_Initial(playerData) -- Inform player. local hint=string.format("Initial") if playerData.difficulty==AIRBOSS.Difficulty.EASY then - local alt,aoa,dist,speed=self:_GetAircraftParameters(playerData, AIRBOSS.PatternStep.BREAKENTRY) - hint=hint..string.format("\nOptimal setup at the break entry is %d feet and %d kts.", UTILS.MetersToFeet(alt), UTILS.MpsToKnots(speed)) + --local alt,aoa,dist,speed=self:_GetAircraftParameters(playerData, AIRBOSS.PatternStep.BREAKENTRY) + --hint=hint..string.format("\nOptimal setup at the break entry is %d feet and %d kts.", UTILS.MetersToFeet(alt), UTILS.MpsToKnots(speed)) end -- Send message for normal and easy difficulty. @@ -4358,6 +4422,8 @@ function AIRBOSS:_Initial(playerData) -- Next step: Break entry. playerData.step=AIRBOSS.PatternStep.BREAKENTRY + playerData.warning=nil + self:_StepHint(playerData) end end @@ -4432,6 +4498,9 @@ function AIRBOSS:_Platform(playerData) playerData.step=AIRBOSS.PatternStep.DIRTYUP end end + + -- Next step hint. + self:_StepHint(playerData) playerData.warning=nil end end @@ -4468,6 +4537,7 @@ function AIRBOSS:_ArcInTurn(playerData) -- Next step: Arc Out Turn. playerData.step=AIRBOSS.PatternStep.ARCOUT playerData.warning=nil + self:_StepHint(playerData) end end @@ -4509,7 +4579,10 @@ function AIRBOSS:_ArcOutTurn(playerData) playerData.step=AIRBOSS.PatternStep.DIRTYUP else -- ERROR! - end + end + + -- Next step hint. + self:_StepHint(playerData) playerData.warning=nil end end @@ -4538,7 +4611,7 @@ function AIRBOSS:_DirtyUp(playerData) -- Get speed hint. -- TODO: Not sure if we already need to be onspeed AoA at this point? - local hintSpeed=self:_SpeedCheck(playerData, speed) + local hintSpeed=self:_SpeedCheck(playerData, speed) -- Message to player. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then @@ -4547,8 +4620,9 @@ function AIRBOSS:_DirtyUp(playerData) end -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). - playerData.step=AIRBOSS.PatternStep.BULLSEYE + playerData.step=AIRBOSS.PatternStep.BULLSEYE playerData.warning=nil + self:_StepHint(playerData) end end @@ -4564,7 +4638,6 @@ function AIRBOSS:_Bullseye(playerData) local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) -- Check that we reached the position. - --if self:_CheckLimits(X, Z, self.Bullseye) then if inzone then -- Debug message. @@ -4588,6 +4661,7 @@ function AIRBOSS:_Bullseye(playerData) -- Next step: Groove Call the ball. playerData.step=AIRBOSS.PatternStep.GROOVE_XX playerData.warning=nil + self:_StepHint(playerData) end end @@ -4627,6 +4701,7 @@ function AIRBOSS:_BreakEntry(playerData) -- Next step: Early Break. playerData.step=AIRBOSS.PatternStep.EARLYBREAK playerData.warning=nil + self:_StepHint(playerData) end end @@ -4677,6 +4752,7 @@ function AIRBOSS:_Break(playerData, part) playerData.step=AIRBOSS.PatternStep.ABEAM end playerData.warning=nil + self:_StepHint(playerData) end end @@ -4759,6 +4835,7 @@ function AIRBOSS:_Abeam(playerData) -- Next step: ninety. playerData.step=AIRBOSS.PatternStep.NINETY playerData.warning=nil + self:_StepHint(playerData) end end @@ -4806,6 +4883,7 @@ function AIRBOSS:_Ninety(playerData) -- Next step: wake. playerData.step=AIRBOSS.PatternStep.WAKE playerData.warning=nil + self:_StepHint(playerData) elseif relheading>90 and self:_CheckLimits(X, Z, self.Wake) then -- Message to player. @@ -4855,6 +4933,7 @@ function AIRBOSS:_Wake(playerData) -- Next step: Final. playerData.step=AIRBOSS.PatternStep.FINAL playerData.warning=nil + self:_StepHint(playerData) end end @@ -4922,6 +5001,7 @@ function AIRBOSS:_Final(playerData) -- Next step: X start & call the ball. playerData.step=AIRBOSS.PatternStep.GROOVE_XX playerData.warning=nil + self:_StepHint(playerData) end end @@ -4980,9 +5060,9 @@ function AIRBOSS:_Groove(playerData) playerData.Tlso=timer.getTime() -- Pilot "405, Hornet Ball, 3.2" - -- TODO: Pilot output should come from pilot in MP. - local text=string.format("Hornet Ball, %.1f", self:_GetFuelState(playerData.unit)/1000) - self:MessageToPlayer(playerData, text, playerData.onboard, "", 3, false, 3) + -- Pilot output should come from pilot. + --local text=string.format("Hornet Ball, %.1f", self:_GetFuelState(playerData.unit)/1000) + --self:MessageToPlayer(playerData, text, playerData.onboard, "", 3, false, 3) -- Store data. playerData.groove.XX=groovedata @@ -5161,7 +5241,7 @@ function AIRBOSS:_GetWire(Ccoord, Lcoord, dx) local FB=self:GetFinalBearing() -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. - local Scoord=Ccoord:Translate(self.carrierparam.sterndist, hdg):Translate(10, FB+90) + local Scoord=Ccoord:Translate(self.carrierparam.sterndist, hdg):Translate(12, FB+90) -- Distance to landing coord. local Ldist=Lcoord:Get2DDistance(Scoord) @@ -5170,18 +5250,25 @@ function AIRBOSS:_GetWire(Ccoord, Lcoord, dx) -- TODO: Maybe add little offset depending on aircraft type. dx=self.carrierparam.wireoffset - -- Corrected distance. - local d=Ldist-dx + -- Landing distance wrt to stern. + local dc=65 + local d=Ldist-dc + + -- Shift wires from stern to their correct position. + local w1=self.carrierparam.wire1+dx + local w2=self.carrierparam.wire2+dx + local w3=self.carrierparam.wire3+dx + local w4=self.carrierparam.wire4+dx - -- Which wire was caught? X>0 since calculated as distance! + -- Which wire was caught? local wire - if d wire=%d.", Ldist, dx, d, wire)) - - return wire -end - ---- Get wire from landing position. --- @param #AIRBOSS self --- @param #number d Distance in meters wrt carrier position where player landed. --- @param #number dx Correction. -function AIRBOSS:_GetWire2(d, dx) - - -- Little offset for the exact wire positions. - dx=dx or self.carrierparam.wireoffset - - -- Which wire was caught? X>0 since calculated as distance! - local wire - if d-dx wire=%d.", d, dx, d-dx, wire)) + self:I(string.format("GetWire: L=%.1f, L-dx=%.1f ==> wire=%d (dx=%.1f)", Ldist, Ldist-dx-dc, wire, dx+dc)) return wire end @@ -6690,7 +6758,7 @@ function AIRBOSS:_Debrief(playerData) -- LSO grade, points, and flight data analyis. local grade, points, analysis=self:_LSOgrade(playerData) - -- My grade. + -- My LSO grade. local mygrade={} --#AIRBOSS.LSOgrade mygrade.grade=grade mygrade.points=points @@ -6698,7 +6766,7 @@ function AIRBOSS:_Debrief(playerData) mygrade.wire=playerData.wire mygrade.Tgroove=playerData.Tgroove - -- Add grade to table. + -- Add LSO grade to table. table.insert(playerData.grades, mygrade) -- LSO grade message. @@ -6708,9 +6776,17 @@ function AIRBOSS:_Debrief(playerData) end text=text..string.format("\nYour detailed debriefing can be found via the F10 radio menu.") self:MessageToPlayer(playerData, text, "LSO", "", 30, true) + + + -- Set step to undefined and check. + playerData.step=AIRBOSS.PatternStep.UNDEFINED - -- Check if boltered or waved off? - if playerData.boltered or playerData.waveoff or playerData.patternwo then + -- Check what happened? + if playerData.patternwo then + + ---------------------- + -- Pattern Wave Off -- + ---------------------- -- Next step? -- TODO: CASE I: After bolter/wo turn left and climb to 600 ft and re-enter the pattern. But do not go to initial but reenter earlier? @@ -6722,67 +6798,118 @@ function AIRBOSS:_Debrief(playerData) if playerData.unit:IsAlive() then - -- TODO: handle case where player landed even though he was waved off! + -- Heading and distance tip. + local heading, distance - if playerData.unit:InAir()==true then + if playerData.case==1 or playerData.case==2 then - -- Heading and distance tip. - local heading, distance - - if playerData.case==1 or playerData.case==2 then - - -- Get heading and distance to initial zone ~3 NM astern. - heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) - distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) - - elseif playerData.case==3 then - - -- Get heading and distance to bullseye zone ~3 NM astern. - local zone=self:_GetZoneBullseye(playerData.case) - - heading=playerData.unit:GetCoordinate():HeadingTo(zone:GetCoordinate()) - distance=playerData.unit:GetCoordinate():Get2DDistance(zone:GetCoordinate()) - - end - - -- Re-enter message. - local text=string.format("fly heading %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) - self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 5) - + -- Next step: Initial again. + playerData.step=AIRBOSS.PatternStep.INITIAL + + -- Get heading and distance to initial zone ~3 NM astern. + heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) + distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) + + elseif playerData.case==3 then + + -- Next step? Bullseye for now. + -- TODO: Could be DIRTY UP or PLATFORM or even back to MARSHAL STACK? + playerData.step=AIRBOSS.PatternStep.BULLSEYE - -- Commencing again. - playerData.step=AIRBOSS.PatternStep.COMMENCING - playerData.warning=nil + -- Get heading and distance to bullseye zone ~3 NM astern. + local zone=self:_GetZoneBullseye(playerData.case) - else + heading=playerData.unit:GetCoordinate():HeadingTo(zone:GetCoordinate()) + distance=playerData.unit:GetCoordinate():Get2DDistance(zone:GetCoordinate()) - if playerData.waveoff then + end + + -- Re-enter message. + local text=string.format("fly heading %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) + self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 5) - -- Airboss talkto! - local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") - self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 2) + else + + -- Unit does not seem to be alive! + -- TODO: What now? + self:I(self.lid..string.format("Player unit not alive!")) - -- Next step undefined. Player landed. - playerData.step=AIRBOSS.PatternStep.UNDEFINED - playerData.warning=nil - - end + end + + elseif playerData.waveoff then + + -------------- + -- Wave Off -- + -------------- + + if playerData.unit:InAir() then + + if playerData.case<3 then + -- Next step: Abeam + playerData.step=AIRBOSS.PatternStep.ABEAM + + else + + -- Next step? Taking Bullseye for now. + playerData.step=AIRBOSS.PatternStep.BULLSEYE + end else - -- Unit does not seem to be alive! - -- TODO: What now? - self:I(self.lid..string.format("Player unit not alive!")) + + -- Airboss talkto! + local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") + self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 2) + + -- Next step undefined. Player landed. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + end + + elseif playerData.boltered then + + -------------- + -- Boltered -- + -------------- + + if playerData.unit:InAir() then + + if playerData.case<3 then + + -- Next step: Abeam + playerData.step=AIRBOSS.PatternStep.ABEAM + + else + + -- Next step? Taking Bullseye for now. + playerData.step=AIRBOSS.PatternStep.BULLSEYE - elseif playerData.landed and not playerData.unit:InAir() then + end + + else + + -- Next step undefined. Player is not in air any more. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + end + + + elseif playerData.landed then - -- Remove player unit from flight and all queues. - self:_RemoveUnitFromFlight(playerData.unit) + ------------ + -- Landed -- + ------------ - -- Message to player. - self:MessageToPlayer(playerData, string.format("Welcome aboard, %s!", playerData.name), "LSO", "", 10) + if not playerData.unit:InAir() then + + -- Remove player unit from flight and all queues. + self:_RemoveUnitFromFlight(playerData.unit) + + -- Message to player. + self:MessageToPlayer(playerData, string.format("Welcome aboard, %s!", playerData.name), "LSO", "", 10) + + end else @@ -6791,16 +6918,74 @@ function AIRBOSS:_Debrief(playerData) -- Next step. playerData.step=AIRBOSS.PatternStep.UNDEFINED - playerData.warning=nil + end -- Increase number of passes. playerData.passes=playerData.passes+1 + -- Next step hint for students if any. + self:_StepHint(playerData) + + -- Reinitialize player data for new approach. + self:_InitPlayer(playerData, playerData.step) + -- Debug message. MESSAGE:New(string.format("Player step %s.", playerData.step), 5, "DEBUG"):ToAllIf(self.Debug) end +--- Hind for flight students about the (next) step. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step Step for which hint is given. +function AIRBOSS:_StepHint(playerData, step) + + -- Set step. + step=step or playerData.step + + -- Message is only for "Flight Students". + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + + -- Get optimal parameters at step. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData, step) + + -- Hint: + local hint="" + + -- Altitude. + if alt then + hint=hint..string.format("\nAltitude=%.1f ft", UTILS.MetersToFeet(alt)) + end + + -- AoA. + if aoa then + hint=hint..string.format("\nAoA=%.1f", aoa) + end + + -- Speed. + if speed then + hint=hint..string.format("\nSpeed=%.1f knots", UTILS.MpsToKnots(speed)) + end + + -- Distance to the boat. + if dist then + hint=hint..string.format("\nDistance=%.1f NM to the boat", UTILS.MetersToNM(dist)) + end + + -- Check if there was actually anything to tell. + if hint~="" then + + -- Compile text if any. + local text=string.format("Optimal setup at next step %s:", step)..hint + + -- Send hint to player. + self:MessageToPlayer(playerData, text, "AIRBOSS", "", 10, false, 2) + + end + + end +end + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- MISC functions ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -7247,7 +7432,7 @@ function AIRBOSS:RadioTransmit(radio, call, loud, delay) else -- Scheduled transmission. - SCHEDULER:New(nil, self.RadioTransmission, {self, radio, call, loud}, delay) + SCHEDULER:New(self, self.RadioTransmission, {radio, call, loud}, delay) end end @@ -7288,7 +7473,7 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration else -- Send onboard number so that player is alerted about the text message. - if receiver==playerData.onboard and not soundoff then + if (receiver==playerData.onboard or receiver=="99") and (not soundoff) then if sender then if sender=="LSO" then self:_Number2Sound(self.LSORadio, receiver, delay) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 45e160e5f..93d171680 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -15,7 +15,7 @@ -- === -- -- ### Author: **funkyfranky** --- ### Special thanks to HighwaymanEd for testing and suggesting improvements! +-- ### Special thanks to **HighwaymanEd** for testing and suggesting improvements! -- -- @module Ops.RecoveryTanker -- @image MOOSE.JPG @@ -1206,7 +1206,7 @@ function RECOVERYTANKER:_ActivateTACAN(delay) if delay and delay>0 then -- Schedule TACAN activation. - SCHEDULER:New(nil, self._ActivateTACAN, {self}, delay) + SCHEDULER:New(self, self._ActivateTACAN, {}, delay) else diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 6413d1d64..f6f56a411 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -1,4 +1,4 @@ ---- **Ops** - (R2.5) - Rescue helo. +--- **Ops** - (R2.5) - Rescue helicopter for carrier operations. -- -- Recue helicopter for carrier operations. -- @@ -14,7 +14,7 @@ -- === -- -- ### Author: **funkyfranky** --- ### Contributions: Flightcontrol (@{AI.#AI_FORMATION} class) +-- ### Contributions: Flightcontrol (@{AI.AI_Formation} class being used here) -- -- @module Ops.RescueHelo -- @image MOOSE.JPG @@ -62,20 +62,20 @@ -- -- # Simple Script -- --- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named "USS Stennis". +-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named "*USS Stennis*". -- --- Secondly, you need to define a recue helicopter group in the mission editor and set it to "LATE ACTIVATED". The name of the group we'll use is "Recue Helo". +-- Secondly, you need to define a recue helicopter group in the mission editor and set it to "**LATE ACTIVATED**". The name of the group we'll use is "*Recue Helo*". -- -- The basic script is very simple and consists of only two lines. -- -- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") -- RescueheloStennis:Start() -- --- The first line will create a new RESCUEHELO object and the second line starts the process. +-- The first line will create a new @{#RESCUEHELO} object via @{#RESCUEHELO.New} and the second line starts the process by calling @{#RESCUEHELO.Start}. -- --- **NOTE** that it is *very important* to define the RESCUEHELO object as **global** variable. Otherwise, the lua garbage collector will kill the formation! +-- **NOTE** that it is *very important* to define the RESCUEHELO object as **global** variable. Otherwise, the lua garbage collector will kill the formation for unknown reasons! -- --- By default, the helo will be spawned on the USS Stennis with hot engines. Then it will take off and go on station on the starboard side of the boat. +-- By default, the helo will be spawned on the *USS Stennis* with hot engines. Then it will take off and go on station on the starboard side of the boat. -- -- Once the helo is out of fuel, it will return to the carrier. When the helo lands, it will be respawned immidiately and go back on station. -- @@ -85,7 +85,7 @@ -- -- # Fine Tuning -- --- The implementation allows to customize quite a few settings easily. +-- The implementation allows to customize quite a few settings easily via user API functions. -- -- ## Takeoff Type -- @@ -136,15 +136,15 @@ -- ## Rescue Operations -- -- By default the rescue helo will start a rescue operation if an aircraft crashes or a pilot ejects in the vicinity of the carrier. --- The standard "rescue zone" has a radius of 30 km around the carrier. The radius can be adjusted via the @{#RESCUEHELO.SetRescueZone}(*radius*) functions, --- where *radius* is the radius of the zone in kilometers. If you use multiple rescue helos in the same mission, you might want to ensure that the radii +-- The standard "rescue zone" has a radius of 15 NM (~28 km) around the carrier. The radius can be adjusted via the @{#RESCUEHELO.SetRescueZone}(*radius*) functions, +-- where *radius* is the radius of the zone in nautical miles. If you use multiple rescue helos in the same mission, you might want to ensure that the radii -- are not overlapping so that two helos try to rescue the same pilot. But it should not hurt either way. -- -- Once the helo reaches the crash site, the rescue operation will last 5 minutes. This time can be changed by @{#RESCUEHELO.SetRescueDuration(*time*), -- where *time* is the duration in minutes. -- --- During the rescue operation, the helo will hover (orbit) over the crash site at a speed of 10 km/h. The speed can be set by @{#RESCUEHELO.SetRescueHoverSpeed}(*speed*), --- where the *speed* is given in km/h. +-- During the rescue operation, the helo will hover (orbit) over the crash site at a speed of 5 knots. The speed can be set by @{#RESCUEHELO.SetRescueHoverSpeed}(*speed*), +-- where the *speed* is given in knots. -- -- If no rescue operations should be carried out by the helo, this option can be completely disabled by using @{#RESCUEHELO.SetRescueOff}(). -- @@ -214,7 +214,7 @@ RESCUEHELO = { --- Class version. -- @field #string version -RESCUEHELO.version="0.9.8" +RESCUEHELO.version="0.9.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -234,7 +234,7 @@ RESCUEHELO.version="0.9.8" --- Create a new RESCUEHELO object. -- @param #RESCUEHELO self --- @param Wrapper.Unit#UNIT carrierunit Carrier unit. +-- @param Wrapper.Unit#UNIT carrierunit Carrier unit object or simply the unit name. -- @param #string helogroupname Name of the late activated rescue helo template group. -- @return #RESCUEHELO RESCUEHELO object. function RESCUEHELO:New(carrierunit, helogroupname) @@ -412,20 +412,20 @@ end --- Set rescue zone radius. Crashed or ejected units inside this radius of the carrier will be rescued if possible. -- @param #RESCUEHELO self --- @param #number radius Radius of rescue zone in kilometers. Default is 30 km. +-- @param #number radius Radius of rescue zone in nautical miles. Default is 15 NM. -- @return #RESCUEHELO self function RESCUEHELO:SetRescueZone(radius) - radius=(radius or 30)*1000 + radius=UTILS.NMToMeters(radius or 15) self.rescuezone=ZONE_UNIT:New("Rescue Zone", self.carrier, radius) return self end --- Set rescue hover speed. -- @param #RESCUEHELO self --- @param #number speed Speed in km/h. Default 10 km/h. +-- @param #number speed Speed in knots. Default 5 kts. -- @return #RESCUEHELO self function RESCUEHELO:SetRescueHoverSpeed(speed) - self.rescuespeed=UTILS.KmphToMps(speed or 10) + self.rescuespeed=UTILS.KnotsToMps(speed or 5) return self end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 59fca2782..487212d03 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -263,7 +263,11 @@ UTILS.FeetToMeters = function(feet) end UTILS.KnotsToKmph = function(knots) - return knots* 1.852 + return knots * 1.852 +end + +UTILS.KmphToKnots = function(knots) + return knots / 1.852 end UTILS.KmphToMps = function( kmph ) From 876b369c0d1441573b5d9495efcb8ac1e2e8ef0a Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 19 Dec 2018 00:40:42 +0100 Subject: [PATCH 108/147] AIRBOSS v0.5.7 --- Moose Development/Moose/Ops/Airboss.lua | 328 +++++++++++------- .../Moose/Wrapper/Controllable.lua | 2 +- 2 files changed, 208 insertions(+), 122 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 86834c16b..b76893d9e 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -6,7 +6,7 @@ -- -- * CASE I, II and III recoveries. -- * Supports human pilots as well as AI flight groups. --- * Automatic LSO grading. +-- * Automatic LSO grading (WIP). -- * Different skill levels from on-the-fly tips for flight students to ziplip for pros. -- * Define recovery time windows with individual recovery cases. -- * Automatic TACAN and ICLS channel setting of carrier. @@ -27,8 +27,8 @@ -- -- **Supported Aircraft:** -- --- * F/A-18C Hornet Lot 20 (player+AI) --- * A-4E Skyhawk Community Mod (player+AI) +-- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI) +-- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI) -- * F/A-18C Hornet (AI) -- * F-14A Tomcat (AI) -- * E-2D Hawkeye (AI) @@ -41,7 +41,7 @@ -- But each aircraft or carrier needs a different set of optimized individual parameters. -- -- **PLEASE NOTE** that his class is work in progress and in an early **alpha** stage. Many/most things work already very nicely but there a lot of cases I did not run into yet. --- Therefore, your *constructive* feedback is both necessary and appreciated! +-- Therefore, your *constructive* feedback is both necessary and appreciated! Find the bugs :) -- -- ### Open Questions? -- @@ -125,6 +125,7 @@ -- @field DCS#Vec3 Corientlast Last known carrier orientation. -- @field Core.Point#COORDINATE Cposition Carrier position. -- @field #string defaultskill Default player skill @{#AIRBOSS.Difficulty}. +-- @field #boolean adinfinitum If true, carrier patrols ad infinitum, i.e. when reaching its last waypoint it starts at waypoint one again. -- @extends Core.Fsm#FSM --- Be the boss! @@ -221,6 +222,9 @@ -- * Default recovery case is set to 1, see @{#AIRBOSS.SetRecoveryCase} -- -- The **second line** starts the AIRBOSS class. If you set options this should happen after the @{#AIRBOSS.New} and before @{#AIRBOSS.Start} command. +-- +-- If no recovery window is set like in the basic example, a window will automatically open 15 minutes after mission start and close again after three hours. +-- The next section explains how to set your own recovery times. -- -- ## Recovery Windows -- @@ -407,6 +411,22 @@ -- -- At these points it is also checked if a player comes too close to another aircraft ahead of him in the pattern. -- +-- ## Grading Points +-- +-- Currently grades are given by as follows +-- +-- * 5.0 Points **\_OK\_**: "Okay underline", given only for a perfect pass, i.e. when no deviations at all were observed by the LSO. The unicorn! +-- * 4.0 Points **OK**: "Okay pass" when only minor () deviations happend. +-- * 3.0 Points **(OK)**: "Fair pass", when only "normal" deviations were detected. +-- * 2.0 Points **--**: "No grade, for larger deviations. +-- +-- Furthermore, we have the cases: +-- +-- * 2.5 Points **B**: "Bolder", when the player landed but did not catch a wire. +-- * 1.0 Points **WO**: "Wave-Off": Player got waved off in the final parts of the groove. +-- * 1.0 Points **PWO**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern. For example, being long in the groove gives a "LIG PWO". +-- * 0.0 Point **CUT**: "Cut pass", when player was waved off but landed anyway. +-- -- # AI Handling -- -- The @{#AIRBOSS} class allows to handle incoming AI units and integrate them into the marshal and landing pattern. @@ -419,11 +439,21 @@ -- -- ## Known Issues -- +-- Dealing with the DCS AI is a big challenge and there is only so much one can do. Please bear this in mind! +-- +-- ### Pattern Updates +-- -- The holding position of the AI is updated regularly when the carrier has changed its position by more then 2.5 NM or changed its course significantly. -- The patterns are realized by orbit or racetrack patterns of the DCS scripting API. -- However, when the position is updated or the marshal stack collapses, it comes to disruptions of the regular orbit because a new waypoint with a new -- orbit task needs to be created. -- +-- ### Recovery Cases +-- +-- The AI performs a very realistic Case I recovery. Therefore, we already have a good Case I and II recovery simulation since the final part of Case II is a +-- Case I recovery. However, I don't think the AI can do a proper Case III recovery. If you give the AI the landing command, it is out of our hands and will +-- always go for a Case I in the final pattern part. Maybe this will improve in future DCS version but right now, there is not much we can do about it. +-- -- # Debugging -- -- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in @@ -447,7 +477,7 @@ -- @field #AIRBOSS AIRBOSS = { ClassName = "AIRBOSS", - Debug = true, + Debug = false, lid = nil, carrier = nil, carriertype = nil, @@ -505,6 +535,7 @@ AIRBOSS = { Corientlast = nil, Cposition = nil, defaultskill = nil, + adinfinitum = nil, } --- Player aircraft types capable of landing on carriers. @@ -1038,19 +1069,22 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.6" +AIRBOSS.version="0.5.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Add voice over fly needs and welcome aboard. +-- TODO: Set magnetic declination function. +-- TODO: Improve trapped wire calculation. +-- TODO: More Hints for Case II/III. -- TODO: Carrier zone with dimensions of carrier. to check if landing happend on deck. -- TODO: Carrier runway zone for fould deck check. -- TODO: Subtitles off options on player level. -- TODO: PWO during case 2/3. Also when too close to other player. -- TODO: Option to filter AI groups for recovery. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! --- TODO: Foul deck check. -- TODO: Persistence of results. -- DONE: First send AI to marshal and then allow them into the landing pattern ==> task function when reaching the waypoint. -- DONE: Extract (static) weather from mission for cloud covery etc. @@ -1108,6 +1142,11 @@ function AIRBOSS:New(carriername, alias) self:E(text) return nil end + + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) -- Set some string id for output to DCS.log file. self.lid=string.format("AIRBOSS %s | ", carriername) @@ -1166,6 +1205,9 @@ function AIRBOSS:New(carriername, alias) -- CCZ 5 NM radius zone around the carrier. self:SetCarrierControlledZone() + -- Carrier patrols its waypoints until the end of time. + self:SetPatrolAdInfinitum(true) + -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then self:_InitStennis() @@ -1604,6 +1646,19 @@ function AIRBOSS:SetDebugModeON() return self end +--- Carrier patrols ad inifintum. If the last waypoint is reached, it will go to waypoint one and repeat its route. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, patrol until the end of time. If false, go along the waypoints once and stop. +-- @return #AIRBOSS self +function AIRBOSS:SetPatrolAdInfinitum(switch) + if switch==false then + self.adinfinitum=false + else + self.adinfinitum=true + end + return self +end + --- Deactivate debug mode. This is also the default setting. -- @param #AIRBOSS self -- @return #AIRBOSS self @@ -1642,7 +1697,7 @@ function AIRBOSS:onafterStart(From, Event, To) -- Current map. local theatre=env.mission.theatre - self:I(self.lid..string.format("Theatre = %s", tostring(theatre))) + self:T2(self.lid..string.format("Theatre = %s", tostring(theatre))) -- Activate TACAN. if self.TACANon then @@ -1676,6 +1731,17 @@ function AIRBOSS:onafterStart(From, Event, To) -- Init patrol route of carrier. self:_PatrolRoute() + + -- Check if no recovery window is set. + if #self.recoverytimes==0 then + + -- Open window in 15 minutes for 3 hours. + local Topen=timer.getAbsTime()+15*60 + local Tclose=Topen+3*60*60 + + -- Add window. + self:AddRecoveryWindow(UTILS.SecondsToClock(Topen), UTILS.SecondsToClock(Tclose)) + end -- Start status check in 1 second. self:__Status(1) @@ -1700,7 +1766,7 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Debug info. local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s", clock, self:GetState(), self.case, self.carrier:GetVelocityKNOTS(), self:GetHeading(), self.currentwp, UTILS.SecondsToClock(self:_GetETAatNextWP())) - self:I(self.lid..text) + self:T(self.lid..text) -- Check recovery times and start/stop recovery mode if necessary. self:_CheckRecoveryTimes() @@ -1859,9 +1925,12 @@ function AIRBOSS:_CheckPlayerPatternDistance(player) -- Check altitude difference? local dalt=math.abs(c2.y-c1.y) + -- 650 feet ~= 200 meters distance between flights + local dcrit=UTILS.FeetToMeters(650) + -- Direction in 30 degrees cone and distance < 200 meters and altitude difference <50 -- TODO: Test parameter values. - if math.abs(rhdg)<30 and dist<200 and dalt<50 then + if math.abs(rhdg)<10 and dist Pattern wave off. + self:T2(self.lid..text) + --MESSAGE:New(text, 20, "DEBUG"):ToAllIf(self.Debug) + + -- Inform player that he is too close. + -- TODO: Pattern wave off? + -- TODO: This function needs a switch so that it is not called over and over again! + --local text=string.format("you're getting too close to the aircraft, %s, ahead of you!\nKeep a min distance of at least 650 ft.", element.onboard) + --self:MessageToPlayer(player, text, "LSO") end end @@ -1978,7 +2056,7 @@ function AIRBOSS:_CheckRecoveryTimes() end -- Debug output. - self:I(self.lid..text) + self:T(self.lid..text) -- Carrier is idle. We need to make sure that incoming flights get the correct recovery info of the next window. if self:IsIdle() then @@ -2017,7 +2095,7 @@ function AIRBOSS:onafterRecoveryCase(From, Event, To, Case, Offset) text=text..string.format(" Holding offset angle %d degrees.", Offset) end MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) - self:I(self.lid..text) + self:T(self.lid..text) -- Set new recovery case. self.case=Case @@ -2047,7 +2125,7 @@ function AIRBOSS:onafterRecoveryStart(From, Event, To, Case, Offset) text=text..string.format(" Holding offset angle %d degrees.", Offset) end MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) - self:I(self.lid..text) + self:T(self.lid..text) -- Switch to case. self:RecoveryCase(Case, Offset) @@ -2061,7 +2139,7 @@ end -- @param #string To To state. function AIRBOSS:onafterRecoveryStop(From, Event, To) -- Debug output. - self:I(self.lid..string.format("Stopping aircraft recovery. Carrier goes to state idle.")) + self:T(self.lid..string.format("Stopping aircraft recovery. Carrier goes to state idle.")) end --- On after "Idle" event. Carrier goes to state "Idle". @@ -2071,7 +2149,7 @@ end -- @param #string To To state. function AIRBOSS:onafterIdle(From, Event, To) -- Debug output. - self:I(self.lid..string.format("Carrier goes to idle.")) + self:T(self.lid..string.format("Carrier goes to idle.")) end --- On after Stop event. Unhandle events. @@ -2115,9 +2193,7 @@ function AIRBOSS._PassingWaypoint(group, airboss, i, final) airboss.currentwp=i -- If final waypoint reached, do route all over again. - if i==final and final>1 then - -- TODO: set task to call this routine again when carrier reaches final waypoint if user chooses to. - -- SetPatrolAdInfinitum user function + if i==final and final>1 and airboss.adinfinitum then airboss:_PatrolRoute() end end @@ -2131,7 +2207,7 @@ function AIRBOSS._ReachedHoldingZone(group, airboss, flight) -- Debug message. local text=string.format("Flight %s reached holding zone.", group:GetName()) MESSAGE:New(text,10):ToAllIf(airboss.Debug) - airboss:I(airboss.lid..text) + airboss:T(airboss.lid..text) -- Debug mark. if airboss.Debug then @@ -2660,7 +2736,7 @@ function AIRBOSS:_CheckQueue() -- Time flight is marshaling. local Tmarshal=timer.getAbsTime()-marshalflight.time - self:I(self.lid..string.format("Marshal time of next group %s = %d seconds", marshalflight.groupname, Tmarshal)) + self:T(self.lid..string.format("Marshal time of next group %s = %d seconds", marshalflight.groupname, Tmarshal)) -- Time (last) flight has entered landing pattern. local Tpattern=9999 @@ -2679,7 +2755,7 @@ function AIRBOSS:_CheckQueue() -- Get time in pattern. Tpattern=timer.getAbsTime()-patternflight.time - self:I(self.lid..string.format("Pattern time of last group %s = %d seconds. # of units=%d.", patternflight.groupname, Tpattern, npunits)) + self:T(self.lid..string.format("Pattern time of last group %s = %d seconds. # of units=%d.", patternflight.groupname, Tpattern, npunits)) end -- Min time in pattern before next aircraft is allowed. @@ -2907,7 +2983,7 @@ function AIRBOSS:_MarshalAI(flight, nstack) ---------------------- -- Debug info. - self:I(self.lid..string.format("Guiding AI flight %s to marshal stack %d-->%d.", groupname, ostack, nstack)) + self:T(self.lid..string.format("Guiding AI flight %s to marshal stack %d-->%d.", groupname, ostack, nstack)) -- Current position. Always good for as the first waypoint. wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") @@ -2953,14 +3029,14 @@ function AIRBOSS:_MarshalAI(flight, nstack) ------------------------ -- Debug info. - self:I(self.lid..string.format("Updating AI flight %s at marshal stack %d-->%d.", groupname, ostack, nstack)) + self:T(self.lid..string.format("Updating AI flight %s at marshal stack %d-->%d.", groupname, ostack, nstack)) -- Current position. Speed expected in km/h. wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedOrbitKmh, {}, "Current Position") - -- Create new waypoint 1 Nm ahead of current positon. + -- Create new waypoint 0.2 Nm ahead of current positon. -- TODO: Set altitude here or take the one of the orbit task? Maybe depends on ostack>nstack or ostack==nstack. - p0=group:GetCoordinate():Translate(UTILS.NMToMeters(1), group:GetHeading()) + p0=group:GetCoordinate():Translate(UTILS.NMToMeters(0.2), group:GetHeading()) end @@ -2991,7 +3067,7 @@ end function AIRBOSS:_LandAI(flight) -- Debug info. - self:I(self.lid..string.format("Landing AI flight %s.", flight.groupname)) + self:T(self.lid..string.format("Landing AI flight %s.", flight.groupname)) -- Aircraft speed when flying the pattern. local Speed=UTILS.KnotsToKmph(274) @@ -3008,8 +3084,8 @@ function AIRBOSS:_LandAI(flight) -- Current positon. wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, Speed, {}, "Current position") - -- Landing waypoint 5 NM behind carrier at 250 ASL. - wp[#wp+1]=self:GetCoordinate():Translate(-UTILS.NMToMeters(5), hdg):SetAltitude(250):WaypointAirLanding(Speed, self.airbase, nil, "Landing") + -- Landing waypoint 5 NM behind carrier at 2000 ft = 610 meters ASL. + wp[#wp+1]=self:GetCoordinate():Translate(-UTILS.NMToMeters(5), hdg):SetAltitude(UTILS.FeetToMeters(2000)):WaypointAirLanding(Speed, self.airbase, nil, "Landing") -- Reinit waypoints. flight.group:WayPointInitialize(wp) @@ -3054,11 +3130,11 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) -- For CCW pattern: First point astern, second ahead of the carrier. - -- First point 1 NM astern. - p1=Carrier --:Translate(-UTILS.NMToMeters(1.0), hdg) + -- First point over carrier. + p1=Carrier - -- Seconds point 2 NM ahead. - p2=Carrier:Translate( UTILS.NMToMeters(1), hdg) + -- Seconds point 1.5 NM ahead. + p2=Carrier:Translate( UTILS.NMToMeters(1.5), hdg) else @@ -3079,7 +3155,7 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) -- For CCW pattern: p1 further astern than p2. -- First point of race track pattern - p1=Carrier:Translate(Dist+UTILS.NMToMeters(10), radial) + p1=Carrier:Translate(Dist+UTILS.NMToMeters(7), radial) -- Second point which is 10 NM further behind. --TODO: check if 10 NM is okay. @@ -3209,7 +3285,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) end -- Debug info. - self:I(string.format("Flight %s case %d is changing marshal stack %d --> %d.", mflight.groupname, mflight.case, mstack, mstack-1)) + self:T(self.lid..string.format("Flight %s case %d is changing marshal stack %d --> %d.", mflight.groupname, mflight.case, mstack, mstack-1)) -- Loop over section members. for _,_sec in pairs(mflight.section) do @@ -3234,17 +3310,17 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) if nopattern then - -- Debug - self:I(self.lid..string.format("Flight %s is leaving stack but not going to pattern.", flight.groupname)) + -- Debug message. + self:T(self.lid..string.format("Flight %s is leaving stack but not going to pattern.", flight.groupname)) -- Set flag to -1. -1 is rather arbitrary. Should not be -100 or positive. flight.flag:Set(-1) else - -- Debug + -- Debug message. local Tmarshal=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) - self:I(self.lid..string.format("Flight %s is leaving marshal after %s and going pattern.", flight.groupname, Tmarshal)) + self:T(self.lid..string.format("Flight %s is leaving marshal after %s and going pattern.", flight.groupname, Tmarshal)) -- Decrease flag. flight.flag:Set(stack-1) @@ -3403,7 +3479,7 @@ function AIRBOSS:_PrintQueue(queue, name) end end end - self:I(self.lid..text) + self:T(self.lid..text) end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3417,7 +3493,7 @@ end function AIRBOSS:_CreateFlightGroup(group) -- Debug info. - self:I(self.lid..string.format("Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName())) + self:T(self.lid..string.format("Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName())) -- New flight. local flight={} --#AIRBOSS.FlightGroup @@ -3463,7 +3539,7 @@ function AIRBOSS:_CreateFlightGroup(group) text=text..string.format("\n[%d] %s onboard #%s", i, name, tostring(element.onboard)) table.insert(flight.elements, element) end - self:I(self.lid..text) + self:T(self.lid..text) -- Onboard if flight.ai then @@ -3539,7 +3615,7 @@ end -- @param #string step (Optional) New player step. Default UNDEFINED. -- @return #AIRBOSS.PlayerData Initialized player data. function AIRBOSS:_InitPlayer(playerData, step) - self:I(self.lid..string.format("Initializing player data for %s callsign %s.", playerData.name, playerData.callsign)) + self:T(self.lid..string.format("Initializing player data for %s callsign %s.", playerData.name, playerData.callsign)) playerData.step=step or AIRBOSS.PatternStep.UNDEFINED playerData.groove={} @@ -3653,7 +3729,7 @@ function AIRBOSS:_RemoveFlightGroup(group) for i,_flight in pairs(self.flights) do local flight=_flight --#AIRBOSS.FlightGroup if flight.groupname==groupname then - self:I(string.format("Removing flight group %s (not in CCA).", groupname)) + self:T(string.format("Removing flight group %s (not in CCA).", groupname)) table.remove(self.flights, i) return end @@ -3672,7 +3748,7 @@ function AIRBOSS:_RemoveFlightFromQueue(queue, flight) -- Check for name. if qflight.groupname==flight.groupname then - self:I(self.lid..string.format("Removing flight group %s from queue.", flight.groupname)) + self:T(self.lid..string.format("Removing flight group %s from queue.", flight.groupname)) table.remove(queue, i) return end @@ -3696,7 +3772,7 @@ function AIRBOSS:_RemoveGroupFromQueue(queue, group) -- Check for name. if flight.groupname==name then - self:I(self.lid..string.format("Removing group %s from queue.", name)) + self:T(self.lid..string.format("Removing group %s from queue.", name)) table.remove(queue, i) return end @@ -4129,7 +4205,7 @@ function AIRBOSS:OnEventLand(EventData) -- Debug output. local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename) - self:I(self.lid..text) + self:T(self.lid..text) MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.Debug) -- Player data. @@ -4185,7 +4261,7 @@ function AIRBOSS:OnEventLand(EventData) -- Debug text. local text=string.format("Player %s AC type %s landed at dist=%.1f m (+offset=%.1f). Trapped wire=%d.", EventData.IniUnitName, _type, dist, self.carrierparam.wireoffset, wire) text=text..string.format("X=%.1f m, Z=%.1f m, rho=%.1f m, phi=%.1f deg.", X, Z, rho, phi) - self:I(self.lid..text) + self:T(self.lid..text) -- We did land. playerData.landed=true @@ -4214,7 +4290,7 @@ function AIRBOSS:OnEventLand(EventData) -- Debug text. local text=string.format("AI %s of type %s landed at dist=%.1f m. Trapped wire=%d.", EventData.IniUnitName, _type, dist, wire) - self:I(self.lid..text) + self:T2(self.lid..text) -- AI always lands ==> remove unit from flight group and queues. self:_RemoveUnitFromFlight(EventData.IniUnit) @@ -4237,9 +4313,9 @@ function AIRBOSS:OnEventCrash(EventData) self:T3(self.lid.."CARSH: player = "..tostring(_playername)) if _unit and _playername then - self:I(self.lid..string.format("Player %s crashed!",_playername)) + self:T(self.lid..string.format("Player %s crashed!",_playername)) else - self:I(self.lid..string.format("AI unit %s crashed!", EventData.IniUnitName)) + self:T2(self.lid..string.format("AI unit %s crashed!", EventData.IniUnitName)) end -- Remove unit from flight and queues. @@ -4260,9 +4336,9 @@ function AIRBOSS:OnEventEjection(EventData) self:T3(self.lid.."EJECT: player = "..tostring(_playername)) if _unit and _playername then - self:I(self.lid..string.format("Player %s ejected!",_playername)) + self:T(self.lid..string.format("Player %s ejected!",_playername)) else - self:I(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) + self:T2(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) end -- Remove unit from flight and queues. @@ -4310,10 +4386,10 @@ function AIRBOSS:_Holding(playerData) if inholdingzone then -- Player is still in holding zone. - self:I("Player is still in the holding zone. Good job.") + self:T2("Player is still in the holding zone. Good job.") else -- Player left the holding zone. - self:I("Player just left the holding zone. Come back!") + self:T("Player just left the holding zone. Come back!") text=text..string.format("You just left the holding zone. Watch your numbers!") playerData.holding=false end @@ -4323,12 +4399,12 @@ function AIRBOSS:_Holding(playerData) -- Player left holding zone if inholdingzone then -- Player is back in the holding zone. - self:I("Player is back in the holding zone after leaving it.") + self:T("Player is back in the holding zone after leaving it.") text=text..string.format("You are back in the holding zone. Now stay there!") playerData.holding=true else -- Player is still outside the holding zone. - self:I("Player still outside the holding zone. What are you doing man?!") + self:T2("Player still outside the holding zone. What are you doing man?!") end elseif playerData.holding==nil then @@ -4340,7 +4416,7 @@ function AIRBOSS:_Holding(playerData) playerData.holding=true -- Debug output. - self:I("Player entered the holding zone for the first time.") + self:T("Player entered the holding zone for the first time.") -- Inform player. text=text..string.format("You arrived at the holding zone.") @@ -4358,7 +4434,7 @@ function AIRBOSS:_Holding(playerData) end else -- Player did not yet arrive in holding zone. - self:I("Waiting for player to arrive in the holding zone.") + self:T2("Waiting for player to arrive in the holding zone.") end end @@ -4410,11 +4486,7 @@ function AIRBOSS:_Initial(playerData) -- Inform player. local hint=string.format("Initial") - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - --local alt,aoa,dist,speed=self:_GetAircraftParameters(playerData, AIRBOSS.PatternStep.BREAKENTRY) - --hint=hint..string.format("\nOptimal setup at the break entry is %d feet and %d kts.", UTILS.MetersToFeet(alt), UTILS.MpsToKnots(speed)) - end - + -- Send message for normal and easy difficulty. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then self:MessageToPlayer(playerData, hint, "MARSHAL") @@ -4690,7 +4762,7 @@ function AIRBOSS:_BreakEntry(playerData) local hintAlt=self:_AltitudeCheck(playerData, alt) -- Get speed hint. - local hintSpeed=self:_SpeedCheck(playerData,speed) + local hintSpeed=self:_SpeedCheck(playerData, speed) -- Message to player. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then @@ -5089,7 +5161,7 @@ function AIRBOSS:_Groove(playerData) -- Debug. local text=string.format("Groove IM=%d m", rho) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:I(self.lid..text) + self:T2(self.lid..text) -- Store data. playerData.groove.IM=groovedata @@ -5106,7 +5178,7 @@ function AIRBOSS:_Groove(playerData) -- Debug local text=string.format("Groove IC=%d m", rho) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:I(self.lid..text) + self:T2(self.lid..text) -- Store data. playerData.groove.IC=groovedata @@ -5138,7 +5210,7 @@ function AIRBOSS:_Groove(playerData) -- Debug. local text=string.format("Groove AR=%d m", rho) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:I(self.lid..text) + self:T2(self.lid..text) -- Store data. playerData.groove.AR=groovedata @@ -5200,13 +5272,13 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) -- Too high or too low? if math.abs(glideslopeError)>1 then - self:I(self.lid..string.format("%s: Wave off due to glide slope error |%.1f| > 1 degree!", playerData.name, glideslopeError)) + self:T(self.lid..string.format("%s: Wave off due to glide slope error |%.1f| > 1 degree!", playerData.name, glideslopeError)) waveoff=true end -- Too far from centerline? if math.abs(lineupError)>3 then - self:I(self.lid..string.format("%s: Wave off due to line up error |%.1f| > 3 degrees!", playerData.name, lineupError)) + self:T(self.lid..string.format("%s: Wave off due to line up error |%.1f| > 3 degrees!", playerData.name, lineupError)) waveoff=true end @@ -5216,10 +5288,10 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) local aoaac=self:_GetAircraftAoA(playerData) -- Check too slow or too fast. if AoAaoaac.Slow then - self:I(self.lid..string.format("%s: Wave off due to AoA %.1f > %.1f!", playerData.name, AoA, aoaac.Slow)) + self:T(self.lid..string.format("%s: Wave off due to AoA %.1f > %.1f!", playerData.name, AoA, aoaac.Slow)) waveoff=true end end @@ -5675,32 +5747,38 @@ function AIRBOSS:_GetZoneHolding(case, stack) -- Pattern alitude. local patternalt, c1, c2=self:_GetMarshalAltitude(stack, case) + -- Select case. if case==1 then -- CASE I - -- Zone 2.5 NM port of carrier with a radius of 3 NM (holding pattern should be < 5 NM). - local R=UTILS.MetersToNM(2.5) - local coord=self:GetCoordinate():Translate(R, 270) + -- Get current carrier heading. + local hdg=self:GetHeading() - zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", coord:GetVec2(), R) + -- Zone 2.5 NM port of carrier with a radius of 2.75 NM (holding pattern should be < 5 NM but we allow 10% error). + local R=UTILS.NMToMeters(2.5) + + -- Create zone. + local coord=self:GetCoordinate():Translate(R, hdg+270) + + zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", coord:GetVec2(), R*1.1) else -- CASE II/II -- Get radial. - local hdg + local radial if case==2 then - hdg=self:GetRadialCase2(false, true) + radial=self:GetRadialCase2(false, true) else - hdg=self:GetRadialCase3(false, true) + radial=self:GetRadialCase3(false, true) end -- Create an array of a square! local p={} - p[1]=c1:Translate(UTILS.NMToMeters(1), hdg-90):GetVec2() --c1 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. - p[2]=c2:Translate(UTILS.NMToMeters(1), hdg-90):GetVec2() --c2 is 10 NM further behind. Also translated 1 NM starboard. - p[3]=c2:Translate(UTILS.NMToMeters(7), hdg+90):GetVec2() --p3 6 NM port of carrier. - p[4]=c1:Translate(UTILS.NMToMeters(7), hdg+90):GetVec2() --p4 6 NM port of carrier. + p[1]=c1:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c1 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. + p[2]=c2:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c2 is 10 NM further behind. Also translated 1 NM starboard. + p[3]=c2:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p3 6 NM port of carrier. + p[4]=c1:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p4 6 NM port of carrier. -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. -- So stay 0-5 NM (+1 NM error margin) port of carrier. @@ -6153,16 +6231,16 @@ end -- @return #string LSO analysis of flight path. function AIRBOSS:_LSOgrade(playerData) - --- Count + --- Count deviations. local function count(base, pattern) return select(2, string.gsub(base, pattern, "")) end -- Analyse flight data and conver to LSO text. - local GXX,nXX=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.XX) --playerData.groove.XX) - local GIM,nIM=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IM) --playerData.groove.IM) - local GIC,nIC=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IC) --playerData.groove.IC) - local GAR,nAR=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.AR) --playerData.groove.AR) + local GXX,nXX=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.XX) + local GIM,nIM=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IM) + local GIC,nIC=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IC) + local GAR,nAR=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.AR) -- Put everything together. local G=GXX.." "..GIM.." ".." "..GIC.." "..GAR @@ -6207,7 +6285,7 @@ function AIRBOSS:_LSOgrade(playerData) text=text.."# of large deviations _ = "..nL.."\n" text=text.."# of normal deviations = "..nN.."\n" text=text.."# of small deviations ( = "..nS.."\n" - self:I(self.lid..text) + self:T2(self.lid..text) --[[ <9 seconds: No Grade @@ -6217,20 +6295,30 @@ function AIRBOSS:_LSOgrade(playerData) >24 seconds: No Grade ]] - if playerData.patternwo or playerData.waveoff then - grade="CUT" - points=1.0 + -- Special cases. + if playerData.patternwo then + -- Pattern Wave Off + grade="PWO" if playerData.lig then - G="LIG PWO" + G="LIG" elseif playerData.patternwo then - G="PWO "..G + G="n/a" end + points=1.0 + elseif playerData.waveoff then + -- Wave Off if playerData.landed then --AIRBOSS wants to talk to you! + grade="CUT" + points=0.0 + else + grade="WO" + points=1.0 end elseif playerData.boltered then + -- Bolter grade="-- (BOLTER)" - points=2.5 + points=2.5 end return grade, points, G @@ -6532,7 +6620,7 @@ end --- Evaluate player's altitude at checkpoint. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. --- @param #number altopt Optimal alitude in meters. +-- @param #number altopt Optimal altitude in meters. -- @return #string Feedback text. -- @return #string Debriefing text. function AIRBOSS:_AltitudeCheck(playerData, altopt) @@ -6693,7 +6781,7 @@ end --- Evaluate player's speed. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. --- @param #number speedopt Optimal speed. +-- @param #number speedopt Optimal speed in m/s. -- @return #string Feedback text. -- @return #string Debriefing text. function AIRBOSS:_SpeedCheck(playerData, speedopt) @@ -6726,7 +6814,7 @@ function AIRBOSS:_SpeedCheck(playerData, speedopt) -- Extend or decrease depending on skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then - hint=hint..string.format(" Optimal altitude is %d ft.", UTILS.MetersToFeet(speedopt)) + hint=hint..string.format(" Optimal speed is %d knots.", UTILS.MpsToKnots(speedopt)) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then --hint=hint.."\n" elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then @@ -6832,7 +6920,7 @@ function AIRBOSS:_Debrief(playerData) -- Unit does not seem to be alive! -- TODO: What now? - self:I(self.lid..string.format("Player unit not alive!")) + self:T2(self.lid..string.format("Player unit not alive!")) end @@ -7399,7 +7487,7 @@ end -- @param #boolean loud If true, play loud sound file version. -- @param #number delay Delay in seconds, before the message is broadcasted. function AIRBOSS:RadioTransmit(radio, call, loud, delay) - self:E({radio=radio, call=call, loud=loud, delay=delay}) + self:F2({radio=radio, call=call, loud=loud, delay=delay}) if (delay==nil) or (delay and delay==0) then @@ -7465,7 +7553,7 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration receiver=receiver or playerData.onboard text=string.format("%s, %s", receiver, message) end - self:I(self.lid..text) + self:T(self.lid..text) if delay and delay>0 then -- Delayed call. @@ -7681,9 +7769,7 @@ function AIRBOSS:_AddF10Commands(_unitName) -- Enable switch so we don't do this twice. self.menuadded[gid]=true - - env.info("FF menu") - + -- Main F10 menu: F10/Airboss// if AIRBOSS.MenuF10[gid]==nil then AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss") @@ -7696,25 +7782,25 @@ function AIRBOSS:_AddF10Commands(_unitName) -- F10/Airboss//F1 Help -------------------------------- local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) - -- F10/Airboss//F1 Help/F1 Skill Level - local _skillPath=missionCommands.addSubMenuForGroup(gid, "Skill Level", _helpPath) - -- F10/Airboss//F1 Help/F1 Skill Level/ - missionCommands.addCommandForGroup(gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) -- F1 - missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) -- F2 - missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) -- F3 - -- F10/Airboss//F1 Help/F2 Mark Zones + -- F10/Airboss//F1 Help/F1 Mark Zones local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) - -- F10/Airboss//F1 Help/F3 Mark Zones/ + -- F10/Airboss//F1 Help/F1 Mark Zones/ missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 missionCommands.addCommandForGroup(gid, "Smoke My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F3 missionCommands.addCommandForGroup(gid, "Flare My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F4 + -- F10/Airboss//F1 Help/F2 Skill Level + local _skillPath=missionCommands.addSubMenuForGroup(gid, "Skill Level", _helpPath) + -- F10/Airboss//F1 Help/F2 Skill Level/ + missionCommands.addCommandForGroup(gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) -- F1 + missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) -- F2 + missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) -- F3 -- F10/Airboss//F1 Help/ - missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F4 - missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._AttitudeMonitor, self, playername) -- F5 - missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) -- F6 - missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) -- F7 - missionCommands.addCommandForGroup(gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) -- F8 + missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._AttitudeMonitor, self, playername) -- F4 + missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) -- F5 + missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) -- F6 + missionCommands.addCommandForGroup(gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) -- F7 ------------------------------------- -- F10/Airboss//F2 Kneeboard @@ -7956,7 +8042,7 @@ function AIRBOSS:_RequestCommence(_unitName) end -- Debug - self:I(self.lid..text) + self:T(self.lid..text) -- Send message. self:MessageToPlayer(playerData, text, "MARSHAL") @@ -8318,7 +8404,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) -- Get recovery times of carrier. local recoverytext="Recovery time windows (max 5):" if #self.recoverytimes==0 then - recoverytext=recoverytext.." none!" + recoverytext=recoverytext.." none." else -- Loop over recovery windows. local rw=0 @@ -8327,7 +8413,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) -- Only include current and future recovery windows. if Tabs=5 then -- Break the loop after 5 recovery times. diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 91bc437a4..24e321308 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -148,7 +148,7 @@ -- * @{#CONTROLLABLE.OptionROEReturnFirePossible} -- * @{#CONTROLLABLE.OptionROEEvadeFirePossible} -- --- ## 5.2) Rule on thread: +-- ## 5.2) Reaction On Thread: -- -- * @{#CONTROLLABLE.OptionROTNoReaction} -- * @{#CONTROLLABLE.OptionROTPassiveDefense} From e91a744bd9a98acaae7e4cf41bfb60ca0348040b Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 19 Dec 2018 07:24:01 +0100 Subject: [PATCH 109/147] Fixed for A2G. Default parameters and proper amount of spawnings. Now also altitude is added for engage. And RTB has min and max speed set. Defaults of speed between 50% and 75% of maximum speed of group. And altitude defaults to 1000 and 1500 meters. --- Moose Development/Moose/AI/AI_A2G_BAI.lua | 17 ++-- Moose Development/Moose/AI/AI_A2G_CAS.lua | 30 +++--- .../Moose/AI/AI_A2G_Dispatcher.lua | 92 +++++++++++-------- Moose Development/Moose/AI/AI_A2G_Engage.lua | 16 ++-- Moose Development/Moose/AI/AI_A2G_Patrol.lua | 33 ++++--- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 19 ++-- Moose Development/Moose/AI/AI_Air.lua | 20 +++- 7 files changed, 141 insertions(+), 86 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index 2806852a1..59da48bd8 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -33,6 +33,8 @@ AI_A2G_BAI = { -- @param Wrapper.Group#GROUP AIGroup -- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. -- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. @@ -40,10 +42,10 @@ AI_A2G_BAI = { -- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. -- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO -- @return #AI_A2G_BAI -function AI_A2G_BAI:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) +function AI_A2G_BAI:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_BAI + local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_BAI return self end @@ -72,8 +74,11 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit -- If it is less than 10km, then attack without a route. -- Otherwise perform a route attack. - local DefenderCoord = DefenderGroup:GetCoordinate() - local TargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + local DefenderCoord = DefenderGroup:GetPointVec3() + DefenderCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + + local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) @@ -84,7 +89,7 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit --- Calculate the target route point. local FromWP = DefenderCoord:WaypointAir( - self.PatrolAltType, + self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, @@ -100,7 +105,7 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit --- Create a route point of type air. local ToWP = ToCoord:Translate( 10000, FromEngageAngle ):WaypointAir( - self.PatrolAltType, + self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index e3ef10419..63562487b 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -14,7 +14,7 @@ --- @type AI_A2G_CAS --- @extends AI.AI_A2G_Engage#AI_A2G_Engage +-- @extends AI.AI_A2G_Patrol#AI_A2G_PATROL --- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. @@ -33,6 +33,8 @@ AI_A2G_CAS = { -- @param Wrapper.Group#GROUP AIGroup -- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. -- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. @@ -40,10 +42,14 @@ AI_A2G_CAS = { -- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. -- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO -- @return #AI_A2G_CAS -function AI_A2G_CAS:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) +function AI_A2G_CAS:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_CAS + local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_CAS + + local RTBSpeedMax = AIGroup:GetSpeedMax() + + self:SetRTBSpeed( RTBSpeedMax * 0.50, RTBSpeedMax * 0.75 ) return self end @@ -72,8 +78,11 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit -- If it is less than 10km, then attack without a route. -- Otherwise perform a route attack. - local DefenderCoord = DefenderGroup:GetCoordinate() - local TargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + local DefenderCoord = DefenderGroup:GetPointVec3() + DefenderCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + + local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) @@ -84,7 +93,7 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit --- Calculate the target route point. local FromWP = DefenderCoord:WaypointAir( - self.PatrolAltType, + self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, @@ -93,16 +102,15 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit EngageRoute[#EngageRoute+1] = FromWP - local ToCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - self:SetTargetDistance( ToCoord ) -- For RTB status check + self:SetTargetDistance( TargetCoord ) -- For RTB status check - local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) + local FromEngageAngle = TargetCoord:GetAngleDegrees( TargetCoord:GetDirectionVec3( DefenderCoord ) ) local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) --- Create a route point of type air. - local ToWP = ToCoord:Translate( EngageDistance, FromEngageAngle ):WaypointAir( - self.PatrolAltType, + local ToWP = TargetCoord:Translate( EngageDistance, FromEngageAngle ):WaypointAir( + self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 0070e3c6e..facc45a66 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -1656,8 +1656,10 @@ do -- AI_A2G_DISPATCHER --- -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param #number EngageMinSpeed The minimum speed at which the SEAD task can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the SEAD task can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the SEAD task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the SEAD task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. -- @usage -- -- -- SEAD Squadron execution. @@ -1666,7 +1668,7 @@ do -- AI_A2G_DISPATCHER -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200 ) -- -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronSead( SquadronName, EngageMinSpeed, EngageMaxSpeed ) + function AI_A2G_DISPATCHER:SetSquadronSead( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -1676,6 +1678,8 @@ do -- AI_A2G_DISPATCHER Sead.Name = SquadronName Sead.EngageMinSpeed = EngageMinSpeed Sead.EngageMaxSpeed = EngageMaxSpeed + Sead.EngageFloorAltitude = EngageFloorAltitude + Sead.EngageCeilingAltitude = EngageCeilingAltitude Sead.Defend = true self:F( { Sead = Sead } ) @@ -1706,12 +1710,12 @@ do -- AI_A2G_DISPATCHER -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. - -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. - -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. - -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. - -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. - -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. + -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. -- @return #AI_A2G_DISPATCHER -- @usage @@ -1729,8 +1733,10 @@ do -- AI_A2G_DISPATCHER local SeadPatrol = DefenderSquadron.SEAD SeadPatrol.Name = SquadronName SeadPatrol.Zone = Zone - SeadPatrol.FloorAltitude = FloorAltitude - SeadPatrol.CeilingAltitude = CeilingAltitude + SeadPatrol.PatrolFloorAltitude = FloorAltitude + SeadPatrol.PatrolCeilingAltitude = CeilingAltitude + SeadPatrol.EngageFloorAltitude = FloorAltitude + SeadPatrol.EngageCeilingAltitude = CeilingAltitude SeadPatrol.PatrolMinSpeed = PatrolMinSpeed SeadPatrol.PatrolMaxSpeed = PatrolMaxSpeed SeadPatrol.EngageMinSpeed = EngageMinSpeed @@ -1747,8 +1753,10 @@ do -- AI_A2G_DISPATCHER --- -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param #number EngageMinSpeed The minimum speed at which the CAS task can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the CAS task can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the CAS task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the CAS task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. -- @usage -- -- -- CAS Squadron execution. @@ -1757,7 +1765,7 @@ do -- AI_A2G_DISPATCHER -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200 ) -- -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronCas( SquadronName, EngageMinSpeed, EngageMaxSpeed ) + function AI_A2G_DISPATCHER:SetSquadronCas( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -1767,6 +1775,8 @@ do -- AI_A2G_DISPATCHER Cas.Name = SquadronName Cas.EngageMinSpeed = EngageMinSpeed Cas.EngageMaxSpeed = EngageMaxSpeed + Cas.EngageFloorAltitude = EngageFloorAltitude + Cas.EngageCeilingAltitude = EngageCeilingAltitude Cas.Defend = true self:F( { Cas = Cas } ) @@ -1798,12 +1808,12 @@ do -- AI_A2G_DISPATCHER -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. - -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. - -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. - -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. - -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. - -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. + -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. -- @return #AI_A2G_DISPATCHER -- @usage @@ -1821,8 +1831,10 @@ do -- AI_A2G_DISPATCHER local CasPatrol = DefenderSquadron.CAS CasPatrol.Name = SquadronName CasPatrol.Zone = Zone - CasPatrol.FloorAltitude = FloorAltitude - CasPatrol.CeilingAltitude = CeilingAltitude + CasPatrol.PatrolFloorAltitude = FloorAltitude + CasPatrol.PatrolCeilingAltitude = CeilingAltitude + CasPatrol.EngageFloorAltitude = FloorAltitude + CasPatrol.EngageCeilingAltitude = CeilingAltitude CasPatrol.PatrolMinSpeed = PatrolMinSpeed CasPatrol.PatrolMaxSpeed = PatrolMaxSpeed CasPatrol.EngageMinSpeed = EngageMinSpeed @@ -1839,8 +1851,10 @@ do -- AI_A2G_DISPATCHER --- -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param #number EngageMinSpeed The minimum speed at which the BAI task can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the BAI task can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the BAI task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the BAI task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. -- @usage -- -- -- BAI Squadron execution. @@ -1849,7 +1863,7 @@ do -- AI_A2G_DISPATCHER -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200 ) -- -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronBai( SquadronName, EngageMinSpeed, EngageMaxSpeed ) + function AI_A2G_DISPATCHER:SetSquadronBai( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -1859,6 +1873,8 @@ do -- AI_A2G_DISPATCHER Bai.Name = SquadronName Bai.EngageMinSpeed = EngageMinSpeed Bai.EngageMaxSpeed = EngageMaxSpeed + Bai.EngageFloorAltitude = EngageFloorAltitude + Bai.EngageCeilingAltitude = EngageCeilingAltitude Bai.Defend = true self:F( { Bai = Bai } ) @@ -1888,12 +1904,12 @@ do -- AI_A2G_DISPATCHER -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. - -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. - -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. - -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. - -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. - -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. + -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. -- @return #AI_A2G_DISPATCHER -- @usage @@ -1911,8 +1927,10 @@ do -- AI_A2G_DISPATCHER local BaiPatrol = DefenderSquadron.BAI BaiPatrol.Name = SquadronName BaiPatrol.Zone = Zone - BaiPatrol.FloorAltitude = FloorAltitude - BaiPatrol.CeilingAltitude = CeilingAltitude + BaiPatrol.PatrolFloorAltitude = FloorAltitude + BaiPatrol.PatrolCeilingAltitude = CeilingAltitude + BaiPatrol.EngageFloorAltitude = FloorAltitude + BaiPatrol.EngageCeilingAltitude = CeilingAltitude BaiPatrol.PatrolMinSpeed = PatrolMinSpeed BaiPatrol.PatrolMaxSpeed = PatrolMaxSpeed BaiPatrol.EngageMinSpeed = EngageMinSpeed @@ -2761,7 +2779,7 @@ do -- AI_A2G_DISPATCHER -- Count the total of defenders on the battlefield. --local DefenderSize = Defender:GetInitialSize() if DefenderTask.Target then - if DefenderTask.Fsm:Is( "Engaging" ) then + --if DefenderTask.Fsm:Is( "Engaging" ) then self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) DefendersTotal = DefendersTotal + DefenderSize if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then @@ -2775,7 +2793,7 @@ do -- AI_A2G_DISPATCHER DefendersEngaged = 0 end end - end + --end end @@ -2923,7 +2941,7 @@ do -- AI_A2G_DISPATCHER local AI_A2G_PATROL = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } - local Fsm = AI_A2G_PATROL[DefenseTaskType]:New( DefenderPatrol, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.Zone, Patrol.FloorAltitude, Patrol.CeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.AltType ) + local Fsm = AI_A2G_PATROL[DefenseTaskType]:New( DefenderPatrol, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.EngageFloorAltitude, Patrol.EngageCeilingAltitude, Patrol.Zone, Patrol.PatrolFloorAltitude, Patrol.PatrolCeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.AltType ) Fsm:SetDispatcher( self ) Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) @@ -3115,7 +3133,7 @@ do -- AI_A2G_DISPATCHER local AI_A2G_ENGAGE = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } - local Fsm = AI_A2G_ENGAGE[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed ) -- AI.AI_A2G_ENGAGE + local Fsm = AI_A2G_ENGAGE[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude ) -- AI.AI_A2G_ENGAGE Fsm:SetDispatcher( self ) Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) diff --git a/Moose Development/Moose/AI/AI_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua index 7c09d1b56..e6be845c3 100644 --- a/Moose Development/Moose/AI/AI_A2G_Engage.lua +++ b/Moose Development/Moose/AI/AI_A2G_Engage.lua @@ -81,8 +81,12 @@ AI_A2G_ENGAGE = { --- Creates a new AI_A2G_ENGAGE object -- @param #AI_A2G_ENGAGE self -- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed (optional, default = 50% of max speed) The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. -- @return #AI_A2G_ENGAGE -function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) +function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) -- Inherits from BASE local self = BASE:Inherit( self, AI_A2G:New( AIGroup ) ) -- #AI_A2G_ENGAGE @@ -90,12 +94,12 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed ) self.Accomplished = false self.Engaging = false - self.EngageMinSpeed = EngageMinSpeed - self.EngageMaxSpeed = EngageMaxSpeed - self.PatrolMinSpeed = EngageMinSpeed - self.PatrolMaxSpeed = EngageMaxSpeed + local SpeedMax = AIGroup:GetSpeedMax() - self.PatrolAltType = "RADIO" + self.EngageMinSpeed = EngageMinSpeed or SpeedMax * 0.5 + self.EngageMaxSpeed = EngageMaxSpeed or SpeedMax * 0.75 + self.EngageFloorAltitude = EngageFloorAltitude or 1000 + self.EngageCeilingAltitude = EngageCeilingAltitude or 1500 self:AddTransition( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua index 185c4a02c..dd9e0342a 100644 --- a/Moose Development/Moose/AI/AI_A2G_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2G_Patrol.lua @@ -99,26 +99,31 @@ AI_A2G_PATROL = { --- Creates a new AI_A2G_PATROL object -- @param #AI_A2G_PATROL self --- @param Wrapper.Group#GROUP AIPatrol --- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. --- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed (optional, default = 50% of max speed) The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. --- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. --- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @param DCS#Altitude PatrolFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO. -- @return #AI_A2G_PATROL -function AI_A2G_PATROL:New( AIPatrol, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) +function AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2G_ENGAGE:New( AIPatrol, EngageMinSpeed, EngageMaxSpeed ) ) -- #AI_A2G_PATROL + local self = BASE:Inherit( self, AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) ) -- #AI_A2G_PATROL + local SpeedMax = AIGroup:GetSpeedMax() + self.PatrolZone = PatrolZone - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude - self.PatrolMinSpeed = PatrolMinSpeed - self.PatrolMaxSpeed = PatrolMaxSpeed + + self.PatrolFloorAltitude = PatrolFloorAltitude or 1000 + self.PatrolCeilingAltitude = PatrolCeilingAltitude or 1500 + self.PatrolMinSpeed = PatrolMinSpeed or SpeedMax * 0.5 + self.PatrolMaxSpeed = PatrolMaxSpeed or SpeedMax * 0.75 -- defafult PatrolAltType to "RADIO" if not specified self.PatrolAltType = PatrolAltType or "RADIO" diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index dfaf89b34..2da77818a 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -14,7 +14,7 @@ --- @type AI_A2G_SEAD --- @extends AI.AI_A2G_Engage#AI_A2G_Engage +-- @extends AI.AI_A2G_Patrol#AI_A2G_PATROL --- Implements the core functions to SEAD intruders. Use the Engage trigger to intercept intruders. @@ -83,6 +83,8 @@ AI_A2G_SEAD = { -- @param Wrapper.Group#GROUP AIGroup -- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. -- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. @@ -90,10 +92,10 @@ AI_A2G_SEAD = { -- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. -- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO -- @return #AI_A2G_SEAD -function AI_A2G_SEAD:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) +function AI_A2G_SEAD:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_SEAD + local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_SEAD return self end @@ -123,8 +125,11 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni -- If it is less than 50km, then attack without a route. -- Otherwise perform a route attack. - local DefenderCoord = DefenderGroup:GetCoordinate() - local TargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + local DefenderCoord = DefenderGroup:GetPointVec3() + DefenderCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + + local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) @@ -137,7 +142,7 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni --- Calculate the target route point. local FromWP = DefenderCoord:WaypointAir( - self.PatrolAltType, + self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, @@ -153,7 +158,7 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni --- Create a route point of type air, 50km from the center of the attack point. local ToWP = ToCoord:Translate( 50000, FromEngageAngle ):WaypointAir( - self.PatrolAltType, + self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 3c808beaa..bbfadc5a0 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -298,6 +298,19 @@ function AI_AIR:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) end +--- Sets (modifies) the minimum and maximum RTB speed of the patrol. +-- @param #AI_AIR self +-- @param DCS#Speed RTBMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed RTBMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @return #AI_AIR self +function AI_AIR:SetRTBSpeed( RTBMinSpeed, RTBMaxSpeed ) + self:F2( { RTBMinSpeed, RTBMaxSpeed } ) + + self.RTBMinSpeed = RTBMinSpeed + self.RTBMaxSpeed = RTBMaxSpeed +end + + --- Sets the floor and ceiling altitude of the patrol. -- @param #AI_AIR self -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. @@ -511,7 +524,7 @@ function AI_AIR:onafterStatus() end if not self:Is("Home") then - self:__Status( 30 ) + self:__Status( 10 ) end end @@ -562,7 +575,7 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) local CurrentCoord = AIGroup:GetCoordinate() local ToTargetCoord = self.HomeAirbase:GetCoordinate() - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + local ToTargetSpeed = math.random( self.RTBMinSpeed, self.RTBMaxSpeed ) local ToAirbaseAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) local Distance = CurrentCoord:Get2DDistance( ToTargetCoord ) @@ -582,9 +595,6 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) true ) - self:F( { Angle = ToAirbaseAngle, ToTargetSpeed = ToTargetSpeed } ) - self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } ) - EngageRoute[#EngageRoute+1] = ToRTBRoutePoint EngageRoute[#EngageRoute+1] = ToRTBRoutePoint From fd6a31992852d8d986ae01238ed99784bd9d5dac Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 19 Dec 2018 17:02:58 +0100 Subject: [PATCH 110/147] AIRBOSS v0.5.7w --- Moose Development/Moose/Ops/Airboss.lua | 332 +++++++++------------ Moose Development/Moose/Ops/RescueHelo.lua | 10 +- 2 files changed, 145 insertions(+), 197 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index b76893d9e..5c8ebadea 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1069,7 +1069,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.7" +AIRBOSS.version="0.5.7w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -2129,7 +2129,6 @@ function AIRBOSS:onafterRecoveryStart(From, Event, To, Case, Offset) -- Switch to case. self:RecoveryCase(Case, Offset) - end --- On after "RecoveryStop" event. Recovery of aircraft is stopped and carrier switches to state "Idle". @@ -3007,12 +3006,7 @@ function AIRBOSS:_MarshalAI(flight, nstack) else -- Get correct radial depending on recovery case including offset. - local radial - if case==2 then - radial=self:GetRadialCase2(false, true) - elseif case==3 then - radial=self:GetRadialCase3(false, true) - end + local radial=self:GetRadial(case, false, true) -- Point in the middle of the race track and a 5 NM more port perpendicular. p0=p2:Translate(UTILS.NMToMeters(5), radial+90):Translate(UTILS.NMToMeters(5), radial) @@ -3083,9 +3077,12 @@ function AIRBOSS:_LandAI(flight) -- Current positon. wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, Speed, {}, "Current position") + + -- Altitude 2000 ft + local alt=UTILS.FeetToMeters(2000) -- Landing waypoint 5 NM behind carrier at 2000 ft = 610 meters ASL. - wp[#wp+1]=self:GetCoordinate():Translate(-UTILS.NMToMeters(5), hdg):SetAltitude(UTILS.FeetToMeters(2000)):WaypointAirLanding(Speed, self.airbase, nil, "Landing") + wp[#wp+1]=self:GetCoordinate():Translate(-UTILS.NMToMeters(5), hdg):SetAltitude(alt):WaypointAirLanding(Speed, self.airbase, nil, "Landing") -- Reinit waypoints. flight.group:WayPointInitialize(wp) @@ -3145,20 +3142,15 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) Dist=UTILS.NMToMeters((stack-1)+angels0+15) -- Get correct radial depending on recovery case including offset. - local radial - if case==2 then - radial=self:GetRadialCase2(false, true) - elseif case==3 then - radial=self:GetRadialCase3(false, true) - end + local radial=self:GetRadial(case, false, true) -- For CCW pattern: p1 further astern than p2. - -- First point of race track pattern + -- First point of race track pattern. + --TODO: check if 7 NM is okay. p1=Carrier:Translate(Dist+UTILS.NMToMeters(7), radial) - -- Second point which is 10 NM further behind. - --TODO: check if 10 NM is okay. + -- Second point. p2=Carrier:Translate(Dist, radial) end @@ -3434,8 +3426,8 @@ function AIRBOSS:_PrintQueue(queue, name) local flight=_flight --#AIRBOSS.FlightGroup -- Timestamp. - --local clock=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) - local clock=timer.getAbsTime()-flight.time + local clock=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + --local clock=timer.getAbsTime()-flight.time -- Recovery case of flight. local case=flight.case -- Stack and stack alt. @@ -3450,10 +3442,7 @@ function AIRBOSS:_PrintQueue(queue, name) local nsec=#flight.section local actype=flight.actype local onboard=flight.onboard - local holding="false" - if flight.holding then - holding="true" - end + local holding=tostring(flight.holding) -- TODO: Include player data. --[[ @@ -3976,13 +3965,13 @@ function AIRBOSS:_CheckPlayerStatus() -- Check if player is too close to another aircraft in the pattern. -- TODO: At which steps is the really necessary. Case II/III? - if playerData.step==AIRBOSS.PatternStep.INITIAL or + if playerData.step==AIRBOSS.PatternStep.INITIAL or playerData.step==AIRBOSS.PatternStep.BREAKENTRY or playerData.step==AIRBOSS.PatternStep.EARLYBREAK or - playerData.step==AIRBOSS.PatternStep.LATEBREAK or - playerData.step==AIRBOSS.PatternStep.ABEAM or - playerData.step==AIRBOSS.PatternStep.GROOVE_XX or - playerData.step==AIRBOSS.PatternStep.GROOVE_IM then + playerData.step==AIRBOSS.PatternStep.LATEBREAK or + playerData.step==AIRBOSS.PatternStep.ABEAM or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM then self:_CheckPlayerPatternDistance(playerData) end @@ -4225,16 +4214,8 @@ function AIRBOSS:OnEventLand(EventData) dist=-dist end - -- Debug output + -- Debug mark of player landing coord. if self.Debug and false then - local hdg=self.carrier:GetHeading()+self.carrierparam.rwyangle - - -- Debug marks of wires. - local w1=self:GetCoordinate():Translate(self.carrierparam.wire1, hdg):MarkToAll("Wire 1a") - local w2=self:GetCoordinate():Translate(self.carrierparam.wire2, hdg):MarkToAll("Wire 2a") - local w3=self:GetCoordinate():Translate(self.carrierparam.wire3, hdg):MarkToAll("Wire 3a") - local w4=self:GetCoordinate():Translate(self.carrierparam.wire4, hdg):MarkToAll("Wire 4a") - -- Debug mark of player landing coord. local lp=coord:MarkToAll("Landing coord.") coord:SmokeGreen() @@ -4261,7 +4242,7 @@ function AIRBOSS:OnEventLand(EventData) -- Debug text. local text=string.format("Player %s AC type %s landed at dist=%.1f m (+offset=%.1f). Trapped wire=%d.", EventData.IniUnitName, _type, dist, self.carrierparam.wireoffset, wire) text=text..string.format("X=%.1f m, Z=%.1f m, rho=%.1f m, phi=%.1f deg.", X, Z, rho, phi) - self:T(self.lid..text) + self:T(self.lid..text) -- We did land. playerData.landed=true @@ -4375,9 +4356,11 @@ function AIRBOSS:_Holding(playerData) -- Check player alt is +-500 feet of assigned pattern alt. local altdiff=playeralt-patternalt local goodalt=math.abs(altdiff)goodalt then + + -- Issue warning. + if not playerData.warning then + text=text..string.format("You just left your assigned altitude. Get back to angels %d.", angels) + playerData.warning=true + end + + else + + -- Back to assigned altitude. + if playerData.warning then + text=text..string.format("Altitude is looking good again.") + playerData.warning=nil + end + + end + elseif playerData.holding==false then -- Player left holding zone @@ -4423,7 +4425,7 @@ function AIRBOSS:_Holding(playerData) -- Feedback on altitude. if goodalt then - text=text..string.format(" Now stay at that altitude.") + text=text..string.format(" Altitude is good.") else if altdiff<0 then text=text..string.format(" But you are too low.") @@ -4432,6 +4434,12 @@ function AIRBOSS:_Holding(playerData) end text=text..string.format(" Currently assigned altitude is %d ft.", UTILS.MetersToFeet(patternalt)) end + + -- No info for the pros. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + text="" + end + else -- Player did not yet arrive in holding zone. self:T2("Waiting for player to arrive in the holding zone.") @@ -4459,8 +4467,13 @@ function AIRBOSS:_Commencing(playerData) -- Commence local text=string.format("Commencing. (Case %d)", playerData.case) - -- Message to all players. - self:MessageToMarshal(text, playerData.onboard, "", 5) + -- Message to all players in Marshal stack. + --self:MessageToMarshal(text, playerData.onboard, "", 5) + + -- Message to player only. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + self:MessageToPlayer(playerData, text, playerData.onboard, "", 5) + end -- Next step: depends on case recovery. if playerData.case==1 then @@ -4484,11 +4497,17 @@ function AIRBOSS:_Initial(playerData) -- Check if player is in initial zone and entering the CASE I pattern. if playerData.unit:IsInZone(self.zoneInitial) then - -- Inform player. - local hint=string.format("Initial") - -- Send message for normal and easy difficulty. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Inform player. + local hint=string.format("Initial") + + -- Hook down for students. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint.." - Hook down!" + end + self:MessageToPlayer(playerData, hint, "MARSHAL") end @@ -4553,12 +4572,15 @@ function AIRBOSS:_Platform(playerData) -- Message to player. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Altitude and speed hint. local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") end -- Next step: depends. - if math.abs(self.holdingoffset)>0 then + if math.abs(self.holdingoffset)>0 and playerData.case>1 then -- Turn to BRC (case II) or FB (case III). playerData.step=AIRBOSS.PatternStep.ARCIN else @@ -4605,9 +4627,11 @@ function AIRBOSS:_ArcInTurn(playerData) local hint=string.format("%s\n%s", playerData.step, hintSpeed) self:MessageToPlayer(playerData, hint, "MARSHAL", "") end + + -- TODO: Hint to turn right and select TACAN FB or BRC. -- Next step: Arc Out Turn. - playerData.step=AIRBOSS.PatternStep.ARCOUT + playerData.step=AIRBOSS.PatternStep.ARCOUT playerData.warning=nil self:_StepHint(playerData) end @@ -4690,6 +4714,8 @@ function AIRBOSS:_DirtyUp(playerData) local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) self:MessageToPlayer(playerData, hint, "MARSHAL", "") end + + --TODO: Hint: Dirty up! Gear, hook and flaps down! -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). playerData.step=AIRBOSS.PatternStep.BULLSEYE @@ -4730,6 +4756,8 @@ function AIRBOSS:_Bullseye(playerData) self:MessageToPlayer(playerData, hint, "MARSHAL", "") end + -- TODO: Hint + -- Next step: Groove Call the ball. playerData.step=AIRBOSS.PatternStep.GROOVE_XX playerData.warning=nil @@ -5442,21 +5470,7 @@ function AIRBOSS:_GetZoneBullseye(case) local distance=UTILS.NMToMeters(3) -- Zone depends on Case recovery. - local radial - if case==2 then - - radial=self:GetRadialCase2(false, false) - - elseif case==3 then - - radial=self:GetRadialCase3(false, false) - - else - - self:E(self.lid.."ERROR: Bullseye zone only for CASE II or III recoveries!") - return nil - - end + local radial=self:GetRadial(case, false, false) -- Get coordinate and vec2. local coord=self:GetCoordinate():Translate(distance, radial) @@ -5481,21 +5495,7 @@ function AIRBOSS:_GetZoneDirtyUp(case) local distance=UTILS.NMToMeters(9) -- Zone depends on Case recovery. - local radial - if case==2 then - - radial=self:GetRadialCase2(false, false) - - elseif case==3 then - - radial=self:GetRadialCase3(false, false) - - else - - self:E(self.lid.."ERROR: Dirty Up zone only for CASE II or III recoveries!") - return nil - - end + local radial=self:GetRadial(case, false, false) -- Get coordinate and vec2. local coord=self:GetCoordinate():Translate(distance, radial) @@ -5520,21 +5520,7 @@ function AIRBOSS:_GetZoneArcOut(case) local distance=UTILS.NMToMeters(12) -- Zone depends on Case recovery. - local radial - if case==2 then - - radial=self:GetRadialCase2(false, false) - - elseif case==3 then - - radial=self:GetRadialCase3(false, false) - - else - - self:E(self.lid.."ERROR: Arc out zone only for CASE II or III recoveries!") - return nil - - end + local radial=self:GetRadial(case, false, false) -- Get coordinate of carrier and translate. local coord=self:GetCoordinate():Translate(distance, radial) @@ -5555,21 +5541,7 @@ function AIRBOSS:_GetZoneArcIn(case) local radius=UTILS.NMToMeters(1) -- Zone depends on Case recovery. - local radial - if case==2 then - - radial=self:GetRadialCase2(false, true) - - elseif case==3 then - - radial=self:GetRadialCase3(false, true) - - else - - self:E(self.lid.."ERROR: Arc in zone only for CASE II or III recoveries!") - return nil - - end + local radial=self:GetRadial(case, false, true) -- Angle between FB/BRC and holding zone. local alpha=math.rad(self.holdingoffset) @@ -5599,21 +5571,7 @@ function AIRBOSS:_GetZonePlatform(case) local radius=UTILS.NMToMeters(1) -- Zone depends on Case recovery. - local radial - if case==2 then - - radial=self:GetRadialCase2(false, true) - - elseif case==3 then - - radial=self:GetRadialCase3(false, true) - - else - - self:E(self.lid.."ERROR: Platform zone only for CASE II or III recoveries!") - return nil - - end + local radial=self:GetRadial(case, false, true) -- Angle between FB/BRC and holding zone. local alpha=math.rad(self.holdingoffset) @@ -5638,21 +5596,9 @@ end function AIRBOSS:_GetZoneCorridor(case) -- Radial and offset. - local radial - local offset + local radial=self:GetRadial(case, false, false) + local offset=self:GetRadial(case, false, true) - -- Select case. - if case==2 then - radial=self:GetRadialCase2(false, false) - offset=self:GetRadialCase2(false, true) - elseif case==3 then - radial=self:GetRadialCase3(false, false) - offset=self:GetRadialCase3(false, true) - else - radial=self:GetRadialCase3(false, false) - offset=self:GetRadialCase3(false, true) - end - -- Angle between radial and offset in rad. local alpha=math.rad(self.holdingoffset) @@ -5766,12 +5712,7 @@ function AIRBOSS:_GetZoneHolding(case, stack) -- CASE II/II -- Get radial. - local radial - if case==2 then - radial=self:GetRadialCase2(false, true) - else - radial=self:GetRadialCase3(false, true) - end + local radial=self:GetRadial(case, false, true) -- Create an array of a square! local p={} @@ -5958,67 +5899,72 @@ function AIRBOSS:GetFinalBearing(magnetic) return fb end ---- Get radial with respect to carrier heading and (optionally) holding offset. This is used in Case II recoveries. +--- Get radial with respect to carrier BRC or FB and (optionally) holding offset. +-- +-- * case=1: radial=FB-180 +-- * case=2: radial=HDG-180 (+offset) +-- * case=3: radial=FB-180 (+offset) +-- -- @param #AIRBOSS self +-- @param #number case Recovery case. -- @param #boolean magnetic If true, magnetic radial is returned. Default is true radial. -- @param #boolean offset If true, inlcude holding offset. +-- @param #boolean inverse Return inverse, i.e. radial-180 degrees. -- @return #number Radial in degrees. -function AIRBOSS:GetRadialCase2(magnetic, offset) +function AIRBOSS:GetRadial(case, magnetic, offset, inverse) - -- Radial wrt to heading of carrier. - local radial=self:GetHeading(magnetic)-180 + -- Case or current case. + case=case or self.case + + -- Radial. + local radial + + -- Select case. + if case==1 then + + -- Get radial. + radial=self:GetFinalBearing(magnetic)-180 + + elseif case==2 then + + -- Radial wrt to heading of carrier. + radial=self:GetHeading(magnetic)-180 + + -- Holding offset angle (+-15 or 30 degrees usually) + if offset then + radial=radial+self.holdingoffset + end + + elseif case==3 then + + -- Radial wrt angled runway. + local radial=self:GetFinalBearing(magnetic)-180 + + -- Holding offset angle (+-15 or 30 degrees usually) + if offset then + radial=radial+self.holdingoffset + end - -- Holding offset angle (+-15 or 30 degrees usually) - if offset then - radial=radial+self.holdingoffset end -- Adjust for negative values. if radial<0 then radial=radial+360 end - - return radial -end ---- Get radial with respect to angled runway and (optionally) holding offset. This is used in Case III recoveries. --- @param #AIRBOSS self --- @param #boolean magnetic If true, magnetic radial is returned. Default is true radial. --- @param #boolean offset If true, inlcude holding offset. --- @return #number Radial in degrees. -function AIRBOSS:GetRadialCase3(magnetic, offset) - - -- Radial wrt angled runway. - local radial=self:GetFinalBearing(magnetic)-180 + -- Inverse? + if inverse then - -- Holding offset angle (+-15 or 30 degrees usually) - if offset then - radial=radial+self.holdingoffset + -- Inverse radial + radial=radial-180 + + -- Adjust for negative values. + if radial<0 then + radial=radial+360 + end + end - - -- Adjust for negative values. - if radial<0 then - radial=radial+360 - end - - return radial -end ---- Get radial, i.e. the final bearing FB-180 degrees. --- @param #AIRBOSS self --- @param #boolean magnetic If true, magnetic radial is returned. Default is true radial. --- @return #number Radial in degrees. -function AIRBOSS:GetRadial(magnetic) - - -- Get radial. - local radial=self:GetFinalBearing(magnetic)-180 - - -- Adjust for negative values. - if radial<0 then - radial=radial+360 - end - - return radial end --- Get relative heading of player wrt carrier. @@ -7042,7 +6988,7 @@ function AIRBOSS:_StepHint(playerData, step) -- Altitude. if alt then - hint=hint..string.format("\nAltitude=%.1f ft", UTILS.MetersToFeet(alt)) + hint=hint..string.format("\nAltitude=%d ft", UTILS.MetersToFeet(alt)) end -- AoA. @@ -7052,19 +6998,19 @@ function AIRBOSS:_StepHint(playerData, step) -- Speed. if speed then - hint=hint..string.format("\nSpeed=%.1f knots", UTILS.MpsToKnots(speed)) + hint=hint..string.format("\nSpeed %d knots", UTILS.MpsToKnots(speed)) end -- Distance to the boat. if dist then - hint=hint..string.format("\nDistance=%.1f NM to the boat", UTILS.MetersToNM(dist)) + hint=hint..string.format("\nDistance to the boat %.1f NM", UTILS.MetersToNM(dist)) end -- Check if there was actually anything to tell. if hint~="" then -- Compile text if any. - local text=string.format("Optimal setup at next step %s:", step)..hint + local text=string.format("Optimal setup at next step %s:%s", step, hint) -- Send hint to player. self:MessageToPlayer(playerData, text, "AIRBOSS", "", 10, false, 2) diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index f6f56a411..40e9c4e4d 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -113,8 +113,10 @@ -- -- ## Home Base -- --- It is possible to define a "home base" other than the aircaft carrier. For example, one could imagine a strike group, and the helo will be spawned from --- another ship which has a helo pad. +-- It is possible to define a "home base" other than the aircaft carrier using the @{#RESCUEHELO.SetHomeBase}(*airbase*) function, where *airbase* is +-- a @{Wrapper.Airbase#AIRBASE} object or simply the name of the airbase. +-- +-- For example, one could imagine a strike group, and the helo will be spawned from another ship which has a helo pad. -- -- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") -- RescueheloStennis:SetHomeBase(AIRBASE:FindByName("USS Normandy")) @@ -130,8 +132,8 @@ -- The position of the helo relative to the mother ship can be tuned via the functions -- -- * @{#RESCUEHELO.SetAltitude}(*altitude*), where *altitude* is the altitude the helo flies at in meters. Default is 70 meters. --- * @{#RESCUEHELO.SetOffsetX}(*distance*)}, where *distance is the distance in the direction of movement of the carrier. Default is 200 meters. --- * @{#RESCUEHELO.SetOffsetZ}(*distance*)}, where *distance is the distance on the starboard side. Default is 100 meters. +-- * @{#RESCUEHELO.SetOffsetX}(*distance*), where *distance is the distance in the direction of movement of the carrier. Default is 200 meters. +-- * @{#RESCUEHELO.SetOffsetZ}(*distance*), where *distance is the distance on the starboard side. Default is 100 meters. -- -- ## Rescue Operations -- From 04722a7d83d3d983c63a31bb5ad6bcc7bead75b3 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 20 Dec 2018 00:49:41 +0100 Subject: [PATCH 111/147] AIRBOSS v0.5.8 --- Moose Development/Moose/Ops/Airboss.lua | 157 +++++++++++++++++---- Moose Development/Moose/Ops/RescueHelo.lua | 7 +- 2 files changed, 132 insertions(+), 32 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 5c8ebadea..ee5f8b017 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -11,7 +11,7 @@ -- * Define recovery time windows with individual recovery cases. -- * Automatic TACAN and ICLS channel setting of carrier. -- * Separate radio channels for LSO and Marshal transmissions. --- * Voice over support for LSO and Marshal radio transmissions. +-- * Voice over support for LSO and Marshal radio transmissions with more than 30 common radio calls. -- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, -- help function (player aircraft attitude, marking of pattern zones etc). -- * Recovery tanker and refueling option via integration of @{Ops.RecoveryTanker} class. @@ -659,6 +659,7 @@ AIRBOSS.PatternStep={ -- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call -- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove. Depart and re-enter." call. -- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call. +-- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard. -- @field #AIRBOSS.RadioCall N0 "Zero" call. -- @field #AIRBOSS.RadioCall N1 "One" call. -- @field #AIRBOSS.RadioCall N2 "Two" call. @@ -775,6 +776,13 @@ AIRBOSS.LSOCall={ subtitle="Paddles, contact", duration=1.0, }, + WELCOMEABOARD={ + file="LSO-WelcomeAboard", + suffix="ogg", + loud=false, + subtitle="Welcome aboard.", + duration=0.9, + }, N0={ file="LSO-N0", suffix="ogg", @@ -849,7 +857,9 @@ AIRBOSS.LSOCall={ --- Marshal radio calls. -- @type AIRBOSS.MarshalCall --- @field #AIRBOSS.RadioCall RADIOCHECK "Marshal, radio check" call. +-- @field #AIRBOSS.RadioCall RADIOCHECK "Radio check" call. +-- @field #AIRBOSS.RadioCall SAYNEEDLES "Say needles" call. +-- @field #AIRBOSS.RadioCall FLYNEEDLES "Fly your needles" call. -- @field #AIRBOSS.RadioCall N0 "Zero" call. -- @field #AIRBOSS.RadioCall N1 "One" call. -- @field #AIRBOSS.RadioCall N2 "Two" call. @@ -865,9 +875,23 @@ AIRBOSS.MarshalCall={ file="MARSHAL-RadioCheck", suffix="ogg", loud=false, - subtitle="Marshal, radio check", + subtitle="Radio check", duration=1.0, }, + SAYNEEDLES={ + file="MARSHAL-SayNeedles", + suffix="ogg", + loud=false, + subtitle="Say needles", + duration=0.9, + }, + FLYNEEDLES={ + file="MARSHAL-FlyYourNeedles", + suffix="ogg", + loud=false, + subtitle="Fly your needles", + duration=0.9, + }, -- TODO: Other voice overs for marshal. N0={ file="LSO-N0", @@ -1069,7 +1093,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.7w" +AIRBOSS.version="0.5.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1623,7 +1647,10 @@ end -- @param #string skill Player skill. Default "Naval Aviator". -- @return #ARIBOSS self function AIRBOSS:SetDefaultPlayerSkill(skill) + + -- Set skill or normal. self.defaultskill=skill or AIRBOSS.Difficulty.NORMAL + -- Check that defualt skill is valid. local gotit=false for _,_skill in pairs(AIRBOSS.Difficulty) do @@ -1631,10 +1658,13 @@ function AIRBOSS:SetDefaultPlayerSkill(skill) gotit=true end end + + -- If invalid user input, fall back to normal. if not gotit then self.defaultskill=AIRBOSS.Difficulty.NORMAL self:E(self.lid..string.format("ERROR: Invalid default skill = %s. Resetting to Naval Aviator.", tostring(skill))) end + return self end @@ -3425,9 +3455,8 @@ function AIRBOSS:_PrintQueue(queue, name) for i,_flight in pairs(queue) do local flight=_flight --#AIRBOSS.FlightGroup - -- Timestamp. + -- Time stamp. local clock=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) - --local clock=timer.getAbsTime()-flight.time -- Recovery case of flight. local case=flight.case -- Stack and stack alt. @@ -3461,7 +3490,7 @@ function AIRBOSS:_PrintQueue(queue, name) k=playerData.waveoff end ]] - text=text..string.format("\n[%d] %s*%d (%s): lead=%s (%d), onboard=%s, flag=%d, case=%d, time=%d, fuel=%d, ai=%s, holding=%s", + text=text..string.format("\n[%d] %s*%d (%s): lead=%s (%d), onboard=%s, flag=%d, case=%d, time=%s, fuel=%d, ai=%s, holding=%s", i, flight.groupname, flight.nunits, actype, lead, nsec, onboard, stack, case, clock, fuel, ai, holding) if flight.holding then text=text..string.format(" stackalt=%d ft", alt) @@ -3586,7 +3615,7 @@ function AIRBOSS:_NewPlayer(unitname) playerData.attitudemonitor=false -- Set difficulty level. - playerData.difficulty=playerData.difficulty or AIRBOSS.Difficulty.EASY + playerData.difficulty=playerData.difficulty or self.defaultskill -- Init stuff for this round. playerData=self:_InitPlayer(playerData) @@ -4131,7 +4160,7 @@ function AIRBOSS:OnEventBirth(EventData) -- Debug output. local text=string.format("AIRBOSS: Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName()) self:T(self.lid..text) - MESSAGE:New(text, 5):ToAllIf(self.Debug or true) + MESSAGE:New(text, 5):ToAllIf(self.Debug) -- Check if aircraft type the player occupies is carrier capable. local rightaircraft=self:_IsCarrierAircraft(_unit) @@ -4145,15 +4174,15 @@ function AIRBOSS:OnEventBirth(EventData) -- Add Menu commands. self:_AddF10Commands(_unitName) + -- Init new player data. + local playerData=self:_NewPlayer(_unitName) + -- Init player data. - self.players[_playername]=self:_NewPlayer(_unitName) - - -- Debug. - if self.Debug and false then - self:_Number2Sound(self.LSORadio, "0123456789", 10) - self:_Number2Sound(self.MarshalRadio, "0123456789", 20) - end + self.players[_playername]=playerData + -- Welcome player message. + self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), "AIRBOSS", "", 5) + end end @@ -4353,9 +4382,28 @@ function AIRBOSS:_Holding(playerData) -- Check if player is in holding zone. local inholdingzone=unit:IsInZone(zoneHolding) - -- Check player alt is +-500 feet of assigned pattern alt. + -- Altitude difference between player and assinged stack. local altdiff=playeralt-patternalt - local goodalt=math.abs(altdiff)goodalt then + if altdiff>altgood then - -- Issue warning. + -- Issue warning for being too high. if not playerData.warning then - text=text..string.format("You just left your assigned altitude. Get back to angels %d.", angels) + text=text..string.format("You left your assigned altitude. Descent to angels %d.", angels) + playerData.warning=true + end + + elseif altdiff<-altgood then + + -- Issue warning for being too low. + if not playerData.warning then + text=text..string.format("You left your assigned altitude. Climb to angels %d.", angels) playerData.warning=true end @@ -4428,11 +4484,12 @@ function AIRBOSS:_Holding(playerData) text=text..string.format(" Altitude is good.") else if altdiff<0 then - text=text..string.format(" But you are too low.") + text=text..string.format(" But you're too low.") else - text=text..string.format(" But you are too high.") + text=text..string.format(" But you're too high.") end - text=text..string.format(" Currently assigned altitude is %d ft.", UTILS.MetersToFeet(patternalt)) + text=text..string.format("\nCurrently assigned altitude is %d ft.", UTILS.MetersToFeet(patternalt)) + playerData.warning=true end -- No info for the pros. @@ -4624,7 +4681,18 @@ function AIRBOSS:_ArcInTurn(playerData) -- Message to player. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint speed. local hint=string.format("%s\n%s", playerData.step, hintSpeed) + + -- Hint turn and set TACAN. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Get inverse magnetic radial without offset ==> FB for Case II or BRC for Case III. + local radial=self:GetRadial(playerData.case, true, false, true) + hint=hint..string.format("\nTurn right and select TACAN %d.", radial) + end + + -- Message to player. self:MessageToPlayer(playerData, hint, "MARSHAL", "") end @@ -4711,12 +4779,23 @@ function AIRBOSS:_DirtyUp(playerData) -- Message to player. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint alt and speed. local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + + -- Hint turn and set TACAN. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint.."\nDirty up! Hook, gear and flaps down." + end + self:MessageToPlayer(playerData, hint, "MARSHAL", "") end - --TODO: Hint: Dirty up! Gear, hook and flaps down! - + -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. + self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.SAYNEEDLES, false, 10) + self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.FLYNEEDLES, false, 15) + -- TODO: Make Fly Bullseye call if no automatic ICLS is active. + -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). playerData.step=AIRBOSS.PatternStep.BULLSEYE playerData.warning=nil @@ -4752,15 +4831,23 @@ function AIRBOSS:_Bullseye(playerData) -- Message to player. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint alt and aoa. local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + + -- Hint follow the needles. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint..string.format("Intercept glide slope and follow the needles.") + end + self:MessageToPlayer(playerData, hint, "MARSHAL", "") end - -- TODO: Hint - -- Next step: Groove Call the ball. - playerData.step=AIRBOSS.PatternStep.GROOVE_XX + playerData.step=AIRBOSS.PatternStep.GROOVE_XX playerData.warning=nil + + -- Stephint should be empty. self:_StepHint(playerData) end end @@ -4838,7 +4925,15 @@ function AIRBOSS:_Break(playerData, part) -- Message to player. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint alt. local hint=string.format("%s %s", playerData.step, hint) + + -- Hint dirty up. + if playerData.difficult==AIRBOSS.Difficulty.EASY and part==AIRBOSS.PatternStep.LATEBREAK then + hint=hint.."Dirty up! Gear down, flaps down. Check hook down." + end + self:MessageToPlayer(playerData, hint, "MARSHAL", "") end @@ -4851,6 +4946,7 @@ function AIRBOSS:_Break(playerData, part) else playerData.step=AIRBOSS.PatternStep.ABEAM end + playerData.warning=nil self:_StepHint(playerData) end @@ -6064,8 +6160,7 @@ function AIRBOSS:_CheckLimits(X, Z, check) -- Debug info. 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) + self:T3(self.lid..text) return next end diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 40e9c4e4d..2d229a1ef 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -138,6 +138,8 @@ -- ## Rescue Operations -- -- By default the rescue helo will start a rescue operation if an aircraft crashes or a pilot ejects in the vicinity of the carrier. +-- This is rescricted to aircraft of the same coaliton as the rescue helo. Enemy (or neutral) pilots will be left on their own. +-- -- The standard "rescue zone" has a radius of 15 NM (~28 km) around the carrier. The radius can be adjusted via the @{#RESCUEHELO.SetRescueZone}(*radius*) functions, -- where *radius* is the radius of the zone in nautical miles. If you use multiple rescue helos in the same mission, you might want to ensure that the radii -- are not overlapping so that two helos try to rescue the same pilot. But it should not hurt either way. @@ -719,9 +721,12 @@ function RESCUEHELO:_OnEventCrashOrEject(EventData) if self.Debug then coord:MarkToCoalition(self.lid..string.format("Crash site of unit %s.", unitname), self.helo:GetCoalition()) end + + -- Check that coalition is the same. + local rightcoalition=EventData.IniGroup:GetCoalition()==self.helo:GetCoalition() -- Only rescue if helo is "running" and not, e.g., rescuing already. - if self:IsRunning() and self.rescueon then + if self:IsRunning() and self.rescueon and rightcoalition then self:Rescue(coord) end From 7f1dcf93d91a055a9a310cea0ae61823f6e403a0 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 20 Dec 2018 16:16:29 +0100 Subject: [PATCH 112/147] AIRBOSS v0.58w --- Moose Development/Moose/Ops/Airboss.lua | 340 +++++++++++++++++------- 1 file changed, 238 insertions(+), 102 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index ee5f8b017..4332dd12d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -37,21 +37,21 @@ -- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) as aircraft and the USS John C. Stennis as carrier. -- The A-4E community mod is also supported in priciple but may need further tweaking of parameters. -- --- The implemenation is kept very general. So other including other aircraft and carriers in future is possible. [*Winter is coming!*](https://forums.eagle.ru/forumdisplay.php?f=395) +-- The implemenation is kept general. So other aircraft and carriers possible in future. [*Winter is coming!*](https://forums.eagle.ru/forumdisplay.php?f=395) -- But each aircraft or carrier needs a different set of optimized individual parameters. -- -- **PLEASE NOTE** that his class is work in progress and in an early **alpha** stage. Many/most things work already very nicely but there a lot of cases I did not run into yet. --- Therefore, your *constructive* feedback is both necessary and appreciated! Find the bugs :) +-- Therefore, your *constructive* feedback is both necessary and appreciated! -- --- ### Open Questions? +-- ### Some Open Questions? -- -- * What are the conditions for a foul deck wave off? -- * What is the next step after a pattern wave off during Case II or III recovery? --- * What is the condition for a "fly through" \\ or \/ LSO grade? +-- * What is the condition for a "fly through" (\\ or /) LSO grade? -- * The above question is one of many regarding LSO grade. If you have more info, please share. -- -- If you know the answer to any of this, please get in touch with me! --- The necessary infrastructure to implement it is most likely already there, but I was not 100% sure about the exact conditions. +-- The necessary infrastructure to implement it is most likely already there, but I am not 100% sure about the exact conditions. -- -- === -- @@ -125,7 +125,8 @@ -- @field DCS#Vec3 Corientlast Last known carrier orientation. -- @field Core.Point#COORDINATE Cposition Carrier position. -- @field #string defaultskill Default player skill @{#AIRBOSS.Difficulty}. --- @field #boolean adinfinitum If true, carrier patrols ad infinitum, i.e. when reaching its last waypoint it starts at waypoint one again. +-- @field #boolean adinfinitum If true, carrier patrols ad infinitum, i.e. when reaching its last waypoint it starts at waypoint one again. +-- @field #number magvar Magnetic declination in degrees. -- @extends Core.Fsm#FSM --- Be the boss! @@ -215,11 +216,14 @@ -- -- This simple script initializes a lot of parameters with default values: -- --- * TACAN channel is set to 74X, see @{#AIRBOSS.SetTACAN} --- * ICSL channel is set to 1, see @{#AIRBOSS.SetICLS} --- * LSO radio is set to 264 MHz FM, see @{#AIRBOSS.SetLSORadio} --- * Marshal radio is set to 305 MHz FM, see @{#AIRBOSS.SetMarshalRadio} --- * Default recovery case is set to 1, see @{#AIRBOSS.SetRecoveryCase} +-- * TACAN channel is set to 74X, see @{#AIRBOSS.SetTACAN}, +-- * ICSL channel is set to 1, see @{#AIRBOSS.SetICLS}, +-- * LSO radio is set to 264 MHz FM, see @{#AIRBOSS.SetLSORadio}, +-- * Marshal radio is set to 305 MHz FM, see @{#AIRBOSS.SetMarshalRadio}, +-- * Default recovery case is set to 1, see @{#AIRBOSS.SetRecoveryCase}, +-- * Carrier Controlled Area (CCA) is set to 50 NM, see @{#AIRBOSS.SetCarrierControlledArea}, +-- * Default player skill "Flight Student" (easy), see @{#AIRBOSS.SetDefaultPlayerSkill}, +-- * Once the carrier reaches its final waypoint, it will restart its route, see @{#AIRBOSS.SetPatrolAdInfinitum}. -- -- The **second line** starts the AIRBOSS class. If you set options this should happen after the @{#AIRBOSS.New} and before @{#AIRBOSS.Start} command. -- @@ -275,7 +279,8 @@ -- -- ### Request Marshal -- --- This radio command can be used to request a stack in the holding pattern from Marshal. Necessary conditions are that the flight is inside the CCZ. +-- This radio command can be used to request a stack in the holding pattern from Marshal. Necessary conditions are that the flight is inside the Carrier Controlled Area (CCA) +-- (see @{#AIRBOSS.SetCarrierControlledArea}). -- Marshal will assign an individual stack for each player group depending on the current or next open recovery case window. -- If multiple players have registered as a section, the section lead will be assigned a stack and is responsible to guide his section to the assigned holding position. -- @@ -307,11 +312,11 @@ -- -- These commands can be used to mark marshal or landing pattern zones. -- --- * **Smoke My Marshal Zone** This smokes the surrounding area of the currently assigned Marshal zone of the player. Player has to be registered in Marshal queue. --- * **Flare My Marshal Zone** Similar to smoke but uses flares to mark the Marshal zone. -- * **Smoke Pattern Zones** Smoke is used to mark the landing pattern zone of the player depending on his recovery case. -- For Case I this is the initial zone. For Case II/III and three these are the Platform, Arc turn, Dirty Up, Bullseye/Initial zones as well as the approach corridor. -- * **Flare Pattern Zones** Similar to smoke but uses flares to mark the pattern zones. +-- * **Smoke Marshal Zone** This smokes the surrounding area of the currently assigned Marshal zone of the player. Player has to be registered in Marshal queue. +-- * **Flare Marshal Zone** Similar to smoke but uses flares to mark the Marshal zone. -- -- ### My Status -- @@ -536,6 +541,7 @@ AIRBOSS = { Cposition = nil, defaultskill = nil, adinfinitum = nil, + magvar = nil, } --- Player aircraft types capable of landing on carriers. @@ -606,6 +612,31 @@ AIRBOSS.CarrierType={ --- Pattern steps. -- @type AIRBOSS.PatternStep +-- @field #string UNDEFINED "Undefined". +-- @field #string REFUELING "Refueling". +-- @field #string SPINNING "Spinning". +-- @field #string COMMENCING "Commencing". +-- @field #string HOLDING "Holding". +-- @field #string PLATFORM "Platform". +-- @field #string ARCIN "Arc Turn In". +-- @field #string ARCOUT "Arc Turn Out". +-- @field #string DIRTYUP "Dirty Up". +-- @field #string BULLSEYE "Bullseye". +-- @field #string INITIAL "Initial". +-- @field #string BREAKENTRY "Break Entry". +-- @field #string EARLYBREAK "Early Break". +-- @field #string LATEBREAK "Late Break". +-- @field #string ABEAM "Abeam". +-- @field #string NINETY "Ninety". +-- @field #string WAKE "Wake". +-- @field #string FINAL "Final". +-- @field #string GROOVE_XX "Groove X". +-- @field #string GROOVE_RB "Groove Roger Ball". +-- @field #string GROOVE_IM "Groove In the Middle". +-- @field #string GROOVE_IC "Groove In Close". +-- @field #string GROOVE_AR "Groove At the Ramp". +-- @field #string GROOVE_IW "Groove In the Wires". +-- @field #string DEBRIEF "Debrief". AIRBOSS.PatternStep={ UNDEFINED="Undefined", REFUELING="Refueling", @@ -657,9 +688,9 @@ AIRBOSS.PatternStep={ -- @field #AIRBOSS.RadioCall ROGERBALL "Roger ball" call. -- @field #AIRBOSS.RadioCall WAVEOFF "Wave off" call -- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call --- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove. Depart and re-enter." call. +-- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove" call. -- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call. --- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard. +-- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard" call. -- @field #AIRBOSS.RadioCall N0 "Zero" call. -- @field #AIRBOSS.RadioCall N1 "One" call. -- @field #AIRBOSS.RadioCall N2 "Two" call. @@ -1093,16 +1124,15 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.8" +AIRBOSS.version="0.5.8w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Player eject and crash debrief "gradings". -- TODO: Add voice over fly needs and welcome aboard. --- TODO: Set magnetic declination function. --- TODO: Improve trapped wire calculation. --- TODO: More Hints for Case II/III. +-- TODO: Improve trapped wire calculation. -- TODO: Carrier zone with dimensions of carrier. to check if landing happend on deck. -- TODO: Carrier runway zone for fould deck check. -- TODO: Subtitles off options on player level. @@ -1110,6 +1140,8 @@ AIRBOSS.version="0.5.8" -- TODO: Option to filter AI groups for recovery. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! -- TODO: Persistence of results. +-- DONE: More Hints for Case II/III. +-- DONE: Set magnetic declination function. -- DONE: First send AI to marshal and then allow them into the landing pattern ==> task function when reaching the waypoint. -- DONE: Extract (static) weather from mission for cloud covery etc. -- DONE: Check distance to players during approach. @@ -1202,6 +1234,9 @@ function AIRBOSS:New(carriername, alias) -- Radio scheduler. self.radiotimer=SCHEDULER:New() + -- Set magnetic declination. + self:SetMagneticDeclination() + -- Set ICSL to channel 1. self:SetICLS() @@ -1597,7 +1632,7 @@ end --- Set number of aircraft units which can be in the landing pattern before the pattern is full. -- @param #AIRBOSS self -- @param #number nmax Max number. Default 4. --- @return #ARIBOSS self +-- @return #AIRBOSS self function AIRBOSS:SetMaxLandingPattern(nmax) self.Nmaxpattern=nmax or 4 return self @@ -1689,6 +1724,17 @@ function AIRBOSS:SetPatrolAdInfinitum(switch) return self end +--- Set the magnetic declination (or variation). By default this is set to the standard declination of the map. +-- @param #AIRBOSS self +-- @param #number declination Declination in degrees or nil for default declination of the map. +-- @return #AIRBOSS self +function AIRBOSS:SetMagneticDeclination(declination) + + self.magvar=declination or UTILS.GetMagneticDeclination() + + return self +end + --- Deactivate debug mode. This is also the default setting. -- @param #AIRBOSS self -- @return #AIRBOSS self @@ -2824,13 +2870,13 @@ function AIRBOSS:_ScanCarrierZone() local unit=_unit --Wrapper.Unit#UNIT -- Necessary conditions to be met: - local airborn=unit:IsAir() and unit:InAir() + local airborne=unit:IsAir() and unit:InAir() local inzone=unit:IsInZone(self.zoneCCA) local friendly=self:GetCoalition()==unit:GetCoalition() local carrierac=self:_IsCarrierAircraft(unit) - -- Check if this an aircraft and that it is airborn and closing in. - if airborn and inzone and friendly and carrierac then + -- Check if this an aircraft and that it is airborne and closing in. + if airborne and inzone and friendly and carrierac then local group=unit:GetGroup() local groupname=group:GetName() @@ -3059,7 +3105,6 @@ function AIRBOSS:_MarshalAI(flight, nstack) wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedOrbitKmh, {}, "Current Position") -- Create new waypoint 0.2 Nm ahead of current positon. - -- TODO: Set altitude here or take the one of the orbit task? Maybe depends on ostack>nstack or ostack==nstack. p0=group:GetCoordinate():Translate(UTILS.NMToMeters(0.2), group:GetHeading()) end @@ -3648,7 +3693,6 @@ function AIRBOSS:_InitPlayer(playerData, step) playerData.Tlso=timer.getTime() playerData.Tgroove=nil playerData.wire=nil - playerData.ballcall=false -- Set us up on final if group name contains "Groove". But only for the first pass. if playerData.group:GetName():match("Groove") and playerData.passes==0 then @@ -4229,58 +4273,67 @@ function AIRBOSS:OnEventLand(EventData) -- Player data. local playerData=self.players[_playername] --#AIRBOSS.PlayerData - -- Coordinate at landing event. - local coord=playerData.unit:GetCoordinate() - - -- Get distances relative to - local X,Z,rho,phi=self:_GetDistances(_unit) + -- Check if player already landed. We dont need a second time. + if playerData.landed then - -- Landing distance to carrier position. - local dist=coord:Get2DDistance(self:GetCoordinate()) + self:E(self.lid..string.format("Player %s just landed a second time.", _playername)) - -- Correct sign if necessary. - if X<0 then - dist=-dist - end + else - -- Debug mark of player landing coord. - if self.Debug and false then + -- Coordinate at landing event. + local coord=playerData.unit:GetCoordinate() + + -- Get distances relative to + local X,Z,rho,phi=self:_GetDistances(_unit) + + -- Landing distance to carrier position. + local dist=coord:Get2DDistance(self:GetCoordinate()) + + -- Correct sign if necessary. + if X<0 then + dist=-dist + end + -- Debug mark of player landing coord. - local lp=coord:MarkToAll("Landing coord.") - coord:SmokeGreen() + if self.Debug and false then + -- Debug mark of player landing coord. + local lp=coord:MarkToAll("Landing coord.") + coord:SmokeGreen() + end + + -- Get wire. + local wire=self:_GetWire(self:GetCoordinate(), coord) + + -- No wire ==> Bolter, Bolter radio call. + if wire>4 then + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.BOLTER) + end + + -- Get time in the groove. + local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData + playerData.Tgroove=timer.getTime()-gdataX0.TGroove + + -- Set player wire + playerData.wire=wire + + -- Aircraft type. + local _type=EventData.IniUnit:GetTypeName() + + -- Debug text. + local text=string.format("Player %s AC type %s landed at dist=%.1f m (+offset=%.1f). Trapped wire=%d.", EventData.IniUnitName, _type, dist, self.carrierparam.wireoffset, wire) + text=text..string.format("X=%.1f m, Z=%.1f m, rho=%.1f m, phi=%.1f deg.", X, Z, rho, phi) + self:T(self.lid..text) + + -- We did land. + playerData.landed=true + + -- Unkonwn step until we now more. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + -- Call trapped function in 3 seconds to make sure we did not bolter. + SCHEDULER:New(self, self._Trapped, {playerData}, 3) + end - - -- Get wire. - local wire=self:_GetWire(self:GetCoordinate(), coord) - - -- No wire ==> Bolter, Bolter radio call. - if wire>4 then - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.BOLTER) - end - - -- Get time in the groove. - local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData - playerData.Tgroove=timer.getTime()-gdataX0.TGroove - - -- Set player wire - playerData.wire=wire - - -- Aircraft type. - local _type=EventData.IniUnit:GetTypeName() - - -- Debug text. - local text=string.format("Player %s AC type %s landed at dist=%.1f m (+offset=%.1f). Trapped wire=%d.", EventData.IniUnitName, _type, dist, self.carrierparam.wireoffset, wire) - text=text..string.format("X=%.1f m, Z=%.1f m, rho=%.1f m, phi=%.1f deg.", X, Z, rho, phi) - self:T(self.lid..text) - - -- We did land. - playerData.landed=true - - -- Unkonwn step until we now more. - playerData.step=AIRBOSS.PatternStep.UNDEFINED - - -- Call trapped function in 3 seconds to make sure we did not bolter. - SCHEDULER:New(self, self._Trapped, {playerData}, 3) else @@ -4695,8 +4748,6 @@ function AIRBOSS:_ArcInTurn(playerData) -- Message to player. self:MessageToPlayer(playerData, hint, "MARSHAL", "") end - - -- TODO: Hint to turn right and select TACAN FB or BRC. -- Next step: Arc Out Turn. playerData.step=AIRBOSS.PatternStep.ARCOUT @@ -5423,12 +5474,30 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) return waveoff end +--- Get "stern" coordinate. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Coordinate at the rundown of the carrier. +function AIRBOSS:_GetSternCoord() + + -- Heading of carrier (true). + local hdg=self.carrier:GetHeading() + + -- Final bearing (true). + local FB=self:GetFinalBearing() + + -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. + local stern=self:GetCoordinate():Translate(self.carrierparam.sterndist, hdg):Translate(12, FB+90) + + return stern +end + --- Get wire from landing position. -- @param #AIRBOSS self -- @param Core.Point#COORDINATE Ccoord Carrier position. -- @param Core.Point#COORDINATE Lcoord Landing position. --- @param #number dx Correction. -function AIRBOSS:_GetWire(Ccoord, Lcoord, dx) +-- @param #number dc Distance correction. +-- @return #number Trapped wire (1-4) or 99 if no wire was trapped. +function AIRBOSS:_GetWire(Ccoord, Lcoord, dc) -- Heading of carrier (true). local hdg=self.carrier:GetHeading() @@ -5437,18 +5506,19 @@ function AIRBOSS:_GetWire(Ccoord, Lcoord, dx) local FB=self:GetFinalBearing() -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. - local Scoord=Ccoord:Translate(self.carrierparam.sterndist, hdg):Translate(12, FB+90) + local Scoord=self:_GetSternCoord() -- Distance to landing coord. local Ldist=Lcoord:Get2DDistance(Scoord) - -- Little offset for the exact wire positions. - -- TODO: Maybe add little offset depending on aircraft type. - dx=self.carrierparam.wireoffset + -- For human (not AI) the lading event is delayed unfortunately. Therefore, we need another correction factor. + dc= dc or 65 - -- Landing distance wrt to stern. - local dc=65 + -- Corrected landing distance wrt to stern. Landing distance needs to be reduced due to delayed landing event for human players. local d=Ldist-dc + + -- Wire offset from stern pos. + local dx=self.carrierparam.wireoffset -- Shift wires from stern to their correct position. local w1=self.carrierparam.wire1+dx @@ -5472,30 +5542,36 @@ function AIRBOSS:_GetWire(Ccoord, Lcoord, dx) if self.Debug then + -- Wire position coodinates. local wp1=Scoord:Translate(w1, FB) local wp2=Scoord:Translate(w2, FB) local wp3=Scoord:Translate(w3, FB) local wp4=Scoord:Translate(w4, FB) + -- Debug marks. wp1:MarkToAll("Wire 1") wp2:MarkToAll("Wire 2") wp3:MarkToAll("Wire 3") wp4:MarkToAll("Wire 4") + -- Mark stern. Scoord:MarkToAll("Stern") + + -- Mark at landing position. Lcoord:MarkToAll(string.format("Landing Point wire=%s", wire)) + -- Smoke landing position. Lcoord:SmokeGreen() + -- Corrected landing position. local Dcoord=Lcoord:Translate(-dc, FB) + -- Smoke corrected landing pos red. Dcoord:SmokeRed() - --local dcoord=Lcoord - + -- Smoke wires. --[[ - Scoord:SmokeGreen() - + Scoord:SmokeGreen() w1:SmokeBlue() w2:SmokeOrange() w3:SmokeRed() @@ -5504,7 +5580,7 @@ function AIRBOSS:_GetWire(Ccoord, Lcoord, dx) end -- Debug output. - self:I(string.format("GetWire: L=%.1f, L-dx=%.1f ==> wire=%d (dx=%.1f)", Ldist, Ldist-dx-dc, wire, dx+dc)) + self:I(string.format("GetWire: L=%.1f, L-dx-dc=%.1f ==> wire=%d (dx=%.1f)", Ldist, Ldist-dx-dc, wire, dx+dc)) return wire end @@ -5517,6 +5593,41 @@ function AIRBOSS:_Trapped(playerData) if playerData.unit:InAir()==false then -- Seems we have successfully landed. + -- Lets see if we can get a good wire. + local unit=playerData.unit + + local coord=unit:GetCoordinate() + + -- Get velocity in km/h + local v=unit:GetVelocityKMH() + + -- Distance + local d=self:GetCoordinate():Get2DDistance(coord) + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + -- Distance to stern pos. + local s=self:GetCoordinate():Get2DDistance(coord) + + -- Debug. + local text=string.format("Player %s _Trapped v=%.1f km/h, d=%.1f m, s=%.1f", playerData.name, v, d, s) + self:E(self.lid..text) + + -- TODO: call this function again until v < threshold. Player comes to a standstill ==> Get wire! + if v>5 then + SCHEDULER:New(self, self._Trapped, {playerData}, 0.1) + return + end + + -- Put some smoke and a mark + if self.Debug then + coord:SmokeBlue() + coord:MarkToAll(text) + stern:MarkToAll("Stern") + end + + -- Get wire. local wire=playerData.wire -- Message to player. @@ -5530,6 +5641,8 @@ function AIRBOSS:_Trapped(playerData) elseif wire==1 then text=text.." Try harder next time!" end + + -- Message to player. self:MessageToPlayer(playerData, text, "LSO", "") -- Debrief. @@ -5539,7 +5652,11 @@ function AIRBOSS:_Trapped(playerData) else --Still in air ==> Boltered! - MESSAGE:New("Player boltered in trapped", 5, "DEBUG") + local text="Player boltered in trapped function." + self:T(self.lid..text) + MESSAGE:New("Player boltered in trapped", 5, "DEBUG"):ToAllIf(self.debug) + + -- Bolter switch on. playerData.boltered=true end @@ -5953,7 +6070,7 @@ function AIRBOSS:GetHeading(magnetic) -- Include magnetic declination. if magnetic then - hdg=hdg-UTILS.GetMagneticDeclination() + hdg=hdg-self.magvar end -- Adjust negative values. @@ -6064,7 +6181,7 @@ function AIRBOSS:GetRadial(case, magnetic, offset, inverse) end --- Get relative heading of player wrt carrier. --- This is the angle between the direction vector of the carrier and the direction vector of the provided unit. +-- This is the angle between the direction/orientation vector of the carrier and the direction/orientation vector of the provided unit. -- Note that this is calculated in the X-Z plane, i.e. the altitude Y is not taken into account. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Player unit. @@ -6125,6 +6242,9 @@ function AIRBOSS:_GetDistances(unit) -- Polar coordinates local rho=math.sqrt(dx*dx+dz*dz) + + + -- Not exactly sure any more what I wanted to calculate here. local phi=math.deg(math.atan2(dz,dx)) if phi<0 then phi=phi+360 @@ -6898,12 +7018,25 @@ function AIRBOSS:_Debrief(playerData) -- Add LSO grade to table. table.insert(playerData.grades, mygrade) - -- LSO grade message. + -- LSO grade: (OK) 3.0 PT - LURIM local text=string.format("%s %.1f PT - %s", grade, points, analysis) - if playerData.wire then + + -- Wire trapped. Not if pattern WI. + if playerData.wire and not playerData.patternwo then text=text..string.format(" %d-wire", playerData.wire) end - text=text..string.format("\nYour detailed debriefing can be found via the F10 radio menu.") + + -- Time in the groove. Only Case I/II and not pattern WO. + if playerData.Tgroove and playerData.case<3 and not playerData.patternwo then + text=text..string.format("\nYour detailed debriefing can be found via the F10 radio menu.") + end + + -- Info text. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + text=text..string.format("\nYour detailed debriefing can be found via the F10 radio menu.") + end + + -- Message. self:MessageToPlayer(playerData, text, "LSO", "", 30, true) @@ -7036,7 +7169,10 @@ function AIRBOSS:_Debrief(playerData) self:_RemoveUnitFromFlight(playerData.unit) -- Message to player. - self:MessageToPlayer(playerData, string.format("Welcome aboard, %s!", playerData.name), "LSO", "", 10) + --self:MessageToPlayer(playerData, string.format("Welcome aboard, %s!", playerData.name), "LSO", "", 10) + + -- Welcome aboard! + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) end @@ -7826,10 +7962,10 @@ function AIRBOSS:_AddF10Commands(_unitName) -- F10/Airboss//F1 Help/F1 Mark Zones local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) -- F10/Airboss//F1 Help/F1 Mark Zones/ - missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 - missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 - missionCommands.addCommandForGroup(gid, "Smoke My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F3 - missionCommands.addCommandForGroup(gid, "Flare My Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F4 + missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 + missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 + missionCommands.addCommandForGroup(gid, "Smoke Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F3 + missionCommands.addCommandForGroup(gid, "Flare Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F4 -- F10/Airboss//F1 Help/F2 Skill Level local _skillPath=missionCommands.addSubMenuForGroup(gid, "Skill Level", _helpPath) -- F10/Airboss//F1 Help/F2 Skill Level/ @@ -7980,12 +8116,12 @@ function AIRBOSS:_RequestMarshal(_unitName) -- Flight group is already in pattern queue. local text=string.format("you are already in the Pattern queue. Marshal request denied!") - self:MessageToPlayer(playerData, text, "MARSHAL") + self:MessageToPlayer(playerData, text, "MARSHAL") elseif not _unit:InAir() then -- Flight group is already in pattern queue. - local text=string.format("you are not airborn. Marshal request denied!") + local text=string.format("you are not airborne. Marshal request denied!") self:MessageToPlayer(playerData, text, "MARSHAL") else @@ -8035,7 +8171,7 @@ function AIRBOSS:_RequestCommence(_unitName) elseif not _unit:InAir() then -- Flight group is already in pattern queue. - text=string.format("%s, you are not airborn. Commence request denied!", playerData.name) + text=string.format("%s, you are not airborne. Commence request denied!", playerData.name) else From afb79391dd400666c5fe3367ef7bf968d3984772 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 21 Dec 2018 00:34:25 +0100 Subject: [PATCH 113/147] AIRBOSS v0.5.9 --- Moose Development/Moose/Ops/Airboss.lua | 179 ++++++++++++++++-------- 1 file changed, 124 insertions(+), 55 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 4332dd12d..476dd3bc4 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -593,12 +593,14 @@ AIRBOSS.CarrierType={ -- @type AIRBOSS.CarrierParameters -- @field #number rwyangle Runway angle in degrees. for carriers with angled deck. For USS Stennis -9 degrees. -- @field #number sterndist Distance in meters from carrier position to stern of carrier. For USS Stennis -150 meters. --- @field #number deckheight Height of deck in meters. For USS Stennis ~22 meters. +-- @field #number deckheight Height of deck in meters. For USS Stennis ~63 ft = 19 meters. -- @field #number wire1 Distance in meters from carrier position to first wire. -- @field #number wire2 Distance in meters from carrier position to second wire. -- @field #number wire3 Distance in meters from carrier position to third wire. -- @field #number wire4 Distance in meters from carrier position to fourth wire. --- @field #number wireoffset Offset in meters for wire calculation. +-- @field #number wireoffset Offset distance from stern/rundown in meters. +-- @field #number rwylength Length of the landing runway in meters. +-- @field #number rwywidth Width of the landing runway in meters. --- Aircraft specific Angle of Attack (AoA) (or alpha) parameters. -- @type AIRBOSS.AircraftAoA @@ -1124,7 +1126,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.8w" +AIRBOSS.version="0.5.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1297,7 +1299,65 @@ function AIRBOSS:New(carriername, alias) self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) end - + + -- Carrier parameter tests. + if false then + -- Stern coordinate. + local FB=self:GetFinalBearing(false) + local hdg=self:GetHeading(false) + local stern=self:_GetSternCoord():SetAltitude(self.carrierparam.deckheight) + local rwy=stern:Translate(self.carrierparam.rwylength, FB):SetAltitude(self.carrierparam.deckheight) + --stern:SmokeGreen() + --bow:SmokeRed() + + local function flareme() + + -- Stern + stern:FlareGreen() + + -- End of runway + --rwy:FlareRed() + + -- Runway half width = 10 m + local r1=stern:Translate(10, FB+90) + r1:FlareWhite() + + -- Carrier pos. + --self:GetCoordinate():FlareYellow() + + + -- Total lendth of carrier from stern to bow. + local bow=stern:Translate(310, hdg) + bow:FlareYellow() + + -- Total width of carrier. + + -- Right 30 meters from stern. + local cR=stern:Translate(30, hdg+90) + cR:FlareYellow() + + -- Left 40 meters from stern. + local cL=stern:Translate(40, hdg-90) + cL:FlareYellow() + + + --[[ + local w1=stern:Translate(46, FB) + local w2=stern:Translate(46+12, FB) + local w3=stern:Translate(46+24, FB) + local w4=stern:Translate(46+35, FB) + w1:FlareWhite() + w2:FlareYellow() + w3:FlareWhite() + w4:FlareYellow() + ]] + + end + + SCHEDULER:New(nil, flareme, {}, 1, 1) + + end + -- If calls should be part of self and individual for different carriers. --[[ -- Init default sound files. @@ -2432,22 +2492,17 @@ function AIRBOSS:_InitStennis() -- Carrier Parameters. self.carrierparam.rwyangle = -9 - self.carrierparam.sterndist =-150 - self.carrierparam.deckheight = 22 + self.carrierparam.sterndist =-153 + self.carrierparam.deckheight = 19 + self.carrierparam.rwylength = 225 + self.carrierparam.rwywidth = 20 - --[[ - self.carrierparam.wire1 =-104 - self.carrierparam.wire2 = -92 - self.carrierparam.wire3 = -80 - self.carrierparam.wire4 = -68 - self.carrierparam.wireoffset = 30 - ]] - - self.carrierparam.wire1 = 0 - self.carrierparam.wire2 = 12 - self.carrierparam.wire3 = 24 - self.carrierparam.wire4 = 36 - self.carrierparam.wireoffset = 50 + -- Wires + self.carrierparam.wire1 = 46 -- Distance from stern to first wire. + self.carrierparam.wire2 = 46+12 + self.carrierparam.wire3 = 46+24 + self.carrierparam.wire4 = 46+35 -- Last wire is strangely one meter closer. + self.carrierparam.wireoffset = 46 -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight. @@ -4302,7 +4357,7 @@ function AIRBOSS:OnEventLand(EventData) end -- Get wire. - local wire=self:_GetWire(self:GetCoordinate(), coord) + local wire=self:_GetWire(coord) -- No wire ==> Bolter, Bolter radio call. if wire>4 then @@ -4331,7 +4386,7 @@ function AIRBOSS:OnEventLand(EventData) playerData.step=AIRBOSS.PatternStep.UNDEFINED -- Call trapped function in 3 seconds to make sure we did not bolter. - SCHEDULER:New(self, self._Trapped, {playerData}, 3) + SCHEDULER:New(self, self._Trapped, {playerData}, 1) end @@ -4346,7 +4401,7 @@ function AIRBOSS:OnEventLand(EventData) local dist=coord:Get2DDistance(self:GetCoordinate()) -- Get wire - local wire=self:_GetWire(self:GetCoordinate(), coord, 0) + local wire=self:_GetWire(coord, 0) -- Aircraft type. local _type=EventData.IniUnit:GetTypeName() @@ -5486,18 +5541,17 @@ function AIRBOSS:_GetSternCoord() local FB=self:GetFinalBearing() -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. - local stern=self:GetCoordinate():Translate(self.carrierparam.sterndist, hdg):Translate(12, FB+90) + local stern=self:GetCoordinate():Translate(self.carrierparam.sterndist, hdg):Translate(7, FB+90) return stern end --- Get wire from landing position. -- @param #AIRBOSS self --- @param Core.Point#COORDINATE Ccoord Carrier position. -- @param Core.Point#COORDINATE Lcoord Landing position. -- @param #number dc Distance correction. -- @return #number Trapped wire (1-4) or 99 if no wire was trapped. -function AIRBOSS:_GetWire(Ccoord, Lcoord, dc) +function AIRBOSS:_GetWire(Lcoord, dc) -- Heading of carrier (true). local hdg=self.carrier:GetHeading() @@ -5516,25 +5570,22 @@ function AIRBOSS:_GetWire(Ccoord, Lcoord, dc) -- Corrected landing distance wrt to stern. Landing distance needs to be reduced due to delayed landing event for human players. local d=Ldist-dc - - -- Wire offset from stern pos. - local dx=self.carrierparam.wireoffset -- Shift wires from stern to their correct position. - local w1=self.carrierparam.wire1+dx - local w2=self.carrierparam.wire2+dx - local w3=self.carrierparam.wire3+dx - local w4=self.carrierparam.wire4+dx + local w1=self.carrierparam.wire1 + local w2=self.carrierparam.wire2 + local w3=self.carrierparam.wire3 + local w4=self.carrierparam.wire4 -- Which wire was caught? local wire - if d wire=%d (dx=%.1f)", Ldist, Ldist-dx-dc, wire, dx+dc)) + self:I(string.format("GetWire: L=%.1f, L-dc=%.1f ==> wire=%d (dc=%.1f)", Ldist, Ldist-dc, wire, dc)) return wire end @@ -5598,8 +5641,8 @@ function AIRBOSS:_Trapped(playerData) local coord=unit:GetCoordinate() - -- Get velocity in km/h - local v=unit:GetVelocityKMH() + -- Get velocity in km/h. We need to substrackt the carrier velocity. + local v=unit:GetVelocityKMH()-self.carrier:GetVelocityKMH() -- Distance local d=self:GetCoordinate():Get2DDistance(coord) @@ -5608,7 +5651,7 @@ function AIRBOSS:_Trapped(playerData) local stern=self:_GetSternCoord() -- Distance to stern pos. - local s=self:GetCoordinate():Get2DDistance(coord) + local s=stern:Get2DDistance(coord) -- Debug. local text=string.format("Player %s _Trapped v=%.1f km/h, d=%.1f m, s=%.1f", playerData.name, v, d, s) @@ -5621,11 +5664,11 @@ function AIRBOSS:_Trapped(playerData) end -- Put some smoke and a mark - if self.Debug then + --if self.Debug then coord:SmokeBlue() coord:MarkToAll(text) stern:MarkToAll("Stern") - end + --end -- Get wire. local wire=playerData.wire @@ -6011,15 +6054,27 @@ function AIRBOSS:_Glideslope(unit, optangle) -- Default is 0. optangle=optangle or 0 - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi = self:_GetDistances(unit) - -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. local h=unit:GetAltitude()-self.carrierparam.deckheight - + + --[[ + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi = self:_GetDistances(unit) -- Distance correction. local offx=self.carrierparam.wire3 or self.carrierparam.sterndist local x=math.abs(self.carrierparam.wire3-X) + ]] + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + -- Ideally we want to land at the 3-wire (or slightly before). + if self.carrierparam.wire3 then + stern:Translate(self.carrierparam.wire3, self:GetFinalBearing(false)) + end + + -- Distance from stern to aircraft. + local x=unit:GetCoordinate():Get2DDistance(stern) -- Glide slope. local glideslope=math.atan(h/x) @@ -6046,6 +6101,18 @@ function AIRBOSS:_Lineup(unit, runway) -- Vector from plane to ref point on boad. local c={x=b.x-a.x, y=0, z=b.z-a.z} + + --[[ + -- Stern coordinate. + local stern=self:_GetSternCoord() + + -- Position of aircraft. + local coord=unit:GetCoordinate() + + -- Vector from stern to aircraft. + local c={x=stern.x-coord.x, y=0, z=stern.z-coord.z} + + ]] -- Current line up and error wrt to final heading of the runway. local lineup=math.deg(math.atan2(c.z, c.x)) @@ -6054,6 +6121,8 @@ function AIRBOSS:_Lineup(unit, runway) if runway then lineup=lineup-self.carrierparam.rwyangle end + + env.info("FF lineup = "..lineup) return lineup, UTILS.VecNorm(c) end @@ -6672,7 +6741,7 @@ function AIRBOSS:_TooFarOutText(X, Z, posData) xtext="close to " end elseif posData.Xmax and X>posData.Xmax then - if posData.LimitXmax>=0 then + if posData.Xmax>=0 then xtext="far ahead of " else xtext="close to " From 34603d69ab67a4c052f2980c15400b9eb98c39f0 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 21 Dec 2018 16:17:42 +0100 Subject: [PATCH 114/147] AIRBOSS v0.5.9w --- Moose Development/Moose/Core/Point.lua | 35 +- Moose Development/Moose/Ops/Airboss.lua | 410 +++++++++++++------- Moose Development/Moose/Utilities/Utils.lua | 17 + 3 files changed, 323 insertions(+), 139 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index a59abbce7..b5cd1ce26 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -460,17 +460,46 @@ do -- COORDINATE -- @param #COORDINATE self -- @param DCS#Distance Distance The Distance to be added in meters. -- @param DCS#Angle Angle The Angle in degrees. Defaults to 0 if not specified (nil). + -- @param #boolean Keepalt If true, keep altitude of original coordinate. Default is that the new coordinate is created at the translated land height. -- @return Core.Point#COORDINATE The new calculated COORDINATE. - function COORDINATE:Translate( Distance, Angle ) + function COORDINATE:Translate( Distance, Angle, Keepalt ) local SX = self.x local SY = self.z local Radians = (Angle or 0) / 180 * math.pi local TX = Distance * math.cos( Radians ) + SX local TY = Distance * math.sin( Radians ) + SY - - return COORDINATE:NewFromVec2( { x = TX, y = TY } ) + + if Keepalt then + return COORDINATE:NewFromVec3( { x = TX, y=self.y, z = TY } ) + else + return COORDINATE:NewFromVec2( { x = TX, y = TY } ) + end end + --- Rotate coordinate in 2D (x,z) space. + -- @param #COORDINATE self + -- @param DCS#Angle Angle Angle of rotation in degrees. + -- @return Core.Point#COORDINATE The rotated coordinate. + function COORDINATE:Rotate2D(Angle) + + if not Angle then + return self + end + + local phi=math.rad(Angle) + + local X=self.z + local Y=self.x + + --slocal R=math.sqrt(X*X+Y*Y) + + local x=X*math.cos(phi)-Y*math.sin(phi) + local y=X*math.sin(phi)+Y*math.cos(phi) + + -- Coordinate assignment looks bit strange but is correct. + return COORDINATE:NewFromVec3({x=y, y=self.y, z=x}) + end + --- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE. -- @param #COORDINATE self -- @param DCS#Distance OuterRadius diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 476dd3bc4..cf12e05c8 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -598,9 +598,11 @@ AIRBOSS.CarrierType={ -- @field #number wire2 Distance in meters from carrier position to second wire. -- @field #number wire3 Distance in meters from carrier position to third wire. -- @field #number wire4 Distance in meters from carrier position to fourth wire. --- @field #number wireoffset Offset distance from stern/rundown in meters. -- @field #number rwylength Length of the landing runway in meters. -- @field #number rwywidth Width of the landing runway in meters. +-- @field #number totlenght Total length of carrier. +-- @field #number totwidthstarboard Total with of the carrier from stern position to starboard side (asymmetric carriers). +-- @field #number totwidthport Total with of the carrier from stern position to port side (asymmetric carriers). --- Aircraft specific Angle of Attack (AoA) (or alpha) parameters. -- @type AIRBOSS.AircraftAoA @@ -1126,17 +1128,17 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.9" +AIRBOSS.version="0.5.9w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Player eject and crash debrief "gradings". --- TODO: Add voice over fly needs and welcome aboard. +-- DONE: Add voice over fly needs and welcome aboard. -- TODO: Improve trapped wire calculation. --- TODO: Carrier zone with dimensions of carrier. to check if landing happend on deck. --- TODO: Carrier runway zone for fould deck check. +-- DONE: Carrier zone with dimensions of carrier. to check if landing happend on deck. +-- DONE: Carrier runway zone for fould deck check. -- TODO: Subtitles off options on player level. -- TODO: PWO during case 2/3. Also when too close to other player. -- TODO: Option to filter AI groups for recovery. @@ -1305,42 +1307,45 @@ function AIRBOSS:New(carriername, alias) -- Stern coordinate. local FB=self:GetFinalBearing(false) local hdg=self:GetHeading(false) - local stern=self:_GetSternCoord():SetAltitude(self.carrierparam.deckheight) - local rwy=stern:Translate(self.carrierparam.rwylength, FB):SetAltitude(self.carrierparam.deckheight) - --stern:SmokeGreen() - --bow:SmokeRed() + + -- Stern pos. + local stern=self:_GetSternCoord() + + -- Bow pos. + local bow=stern:Translate(self.carrierparam.totlenght, hdg) + + -- End of rwy. + local rwy=stern:Translate(self.carrierparam.rwylength, FB, true) + local function flareme() + + -- Carrier pos. + self:GetCoordinate():FlareYellow() -- Stern stern:FlareGreen() - - -- End of runway - --rwy:FlareRed() - - -- Runway half width = 10 m - local r1=stern:Translate(10, FB+90) - r1:FlareWhite() - - -- Carrier pos. - --self:GetCoordinate():FlareYellow() - - - -- Total lendth of carrier from stern to bow. - local bow=stern:Translate(310, hdg) + + -- Bow bow:FlareYellow() - -- Total width of carrier. + -- Runway half width = 10 m. + local r1=stern:Translate(self.carrierparam.rwywidth*0.5, FB+90) + local r2=stern:Translate(self.carrierparam.rwywidth*0.5, FB-90) + r1:FlareWhite() + r2:FlareWhite() + + -- End of runway. + rwy:FlareRed() -- Right 30 meters from stern. - local cR=stern:Translate(30, hdg+90) + local cR=stern:Translate(self.carrierparam.totstarboard, hdg+90) cR:FlareYellow() -- Left 40 meters from stern. - local cL=stern:Translate(40, hdg-90) + local cL=stern:Translate(self.carrierparam.totport, hdg-90) cL:FlareYellow() - - + --[[ local w1=stern:Translate(46, FB) local w2=stern:Translate(46+12, FB) @@ -1700,7 +1705,7 @@ end --- Handle AI aircraft. -- @param #AIRBOSS self --- @return #ARIBOSS self +-- @return #AIRBOSS self function AIRBOSS:SetHandleAION() self.handleai=true return self @@ -2491,18 +2496,24 @@ end function AIRBOSS:_InitStennis() -- Carrier Parameters. - self.carrierparam.rwyangle = -9 self.carrierparam.sterndist =-153 self.carrierparam.deckheight = 19 + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlenght=310 -- Wiki says 332.8 meters overall length. + self.carrierparam.totwidthport=40 -- Wiki says 76.8 meters overall beam. + self.carrierparam.totwidthstarboard=30 + + -- Landing runway. + self.carrierparam.rwyangle = -9 self.carrierparam.rwylength = 225 self.carrierparam.rwywidth = 20 - -- Wires + -- Wires. self.carrierparam.wire1 = 46 -- Distance from stern to first wire. self.carrierparam.wire2 = 46+12 self.carrierparam.wire3 = 46+24 self.carrierparam.wire4 = 46+35 -- Last wire is strangely one meter closer. - self.carrierparam.wireoffset = 46 -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight. @@ -3126,10 +3137,10 @@ function AIRBOSS:_MarshalAI(flight, nstack) -- Initial point 7 NM and a bit port of carrier. -- TODO: Test and tune! - local pE=Carrier:SetAltitude(altitude):Translate(UTILS.NMToMeters(7), hdg-30) + local pE=Carrier:Translate(UTILS.NMToMeters(7), hdg-30):SetAltitude(altitude) -- Entry point 5 NM port and slightly astern the boat. - p0=Carrier:SetAltitude(altitude):Translate(UTILS.NMToMeters(5*math.sqrt(2)), hdg-135) + p0=Carrier:Translate(UTILS.NMToMeters(5*math.sqrt(2)), hdg-135):SetAltitude(altitude) -- Waypoint ahead of carrier's holding zone. wp[#wp+1]=pE:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") @@ -3140,7 +3151,7 @@ function AIRBOSS:_MarshalAI(flight, nstack) local radial=self:GetRadial(case, false, true) -- Point in the middle of the race track and a 5 NM more port perpendicular. - p0=p2:Translate(UTILS.NMToMeters(5), radial+90):Translate(UTILS.NMToMeters(5), radial) + p0=p2:Translate(UTILS.NMToMeters(5), radial+90):Translate(UTILS.NMToMeters(5), radial, true) -- Entering Case II/III marshal pattern waypoint. wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case II/III Marshal Pattern") @@ -3160,7 +3171,7 @@ function AIRBOSS:_MarshalAI(flight, nstack) wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedOrbitKmh, {}, "Current Position") -- Create new waypoint 0.2 Nm ahead of current positon. - p0=group:GetCoordinate():Translate(UTILS.NMToMeters(0.2), group:GetHeading()) + p0=group:GetCoordinate():Translate(UTILS.NMToMeters(0.2), group:GetHeading(), true) end @@ -4308,11 +4319,16 @@ function AIRBOSS:OnEventLand(EventData) -- Check if aircraft landed on the right airbase. if airbasename==self.airbase:GetName() then + + -- Stern coordinate at the rundown. + local stern=self:_GetSternCoord() + + local zoneCarrier=self:_GetZoneCarrierBox() -- Check if player or AI landed. if _unit and _playername then -- Human Player landed. - + -- Get info. local _uid=_unit:GetID() local _group=_unit:GetGroup() @@ -4328,66 +4344,64 @@ function AIRBOSS:OnEventLand(EventData) -- Player data. local playerData=self.players[_playername] --#AIRBOSS.PlayerData - -- Check if player already landed. We dont need a second time. - if playerData.landed then + -- Check that player landed on the carrier. + if _unit:IsInZone(zoneCarrier) then - self:E(self.lid..string.format("Player %s just landed a second time.", _playername)) - - else - - -- Coordinate at landing event. - local coord=playerData.unit:GetCoordinate() - - -- Get distances relative to - local X,Z,rho,phi=self:_GetDistances(_unit) + -- Check if player already landed. We dont need a second time. + if playerData.landed then - -- Landing distance to carrier position. - local dist=coord:Get2DDistance(self:GetCoordinate()) + self:E(self.lid..string.format("Player %s just landed a second time.", _playername)) - -- Correct sign if necessary. - if X<0 then - dist=-dist - end + else - -- Debug mark of player landing coord. - if self.Debug and false then + -- Coordinate at landing event. + local coord=playerData.unit:GetCoordinate() + + -- Get distances relative to + local X,Z,rho,phi=self:_GetDistances(_unit) + + -- Landing distance wrt to stern position. + local dist=coord:Get2DDistance(stern) + + -- Debug mark of player landing coord. - local lp=coord:MarkToAll("Landing coord.") - coord:SmokeGreen() + if self.Debug and false then + -- Debug mark of player landing coord. + local lp=coord:MarkToAll("Landing coord.") + coord:SmokeGreen() + end + + -- Get wire. We additionally shift the landing coord back because landing event for players is unfortunately delayed. + local wire=self:_GetWire(coord, 65) + + -- No wire ==> Bolter, Bolter radio call. + -- TODO: might need a better place for this. or check + if wire>4 then + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.BOLTER) + end + + -- Get time in the groove. + local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData + playerData.Tgroove=timer.getTime()-gdataX0.TGroove + + -- Debug text. + local text=string.format("Player %s AC type %s landed at dist=%.1f m. Trapped wire=%d.", playerData.name, playerData.actype, dist, wire) + text=text..string.format("X=%.1f m, Z=%.1f m, rho=%.1f m, phi=%.1f deg.", X, Z, rho, phi) + self:T(self.lid..text) + + -- We did land. + playerData.landed=true + + -- Unkonwn step until we now more. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + -- Call trapped function in 1 second to make sure we did not bolter. + SCHEDULER:New(self, self._Trapped, {playerData}, 1) + end - -- Get wire. - local wire=self:_GetWire(coord) - - -- No wire ==> Bolter, Bolter radio call. - if wire>4 then - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.BOLTER) - end - - -- Get time in the groove. - local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData - playerData.Tgroove=timer.getTime()-gdataX0.TGroove - - -- Set player wire - playerData.wire=wire - - -- Aircraft type. - local _type=EventData.IniUnit:GetTypeName() - - -- Debug text. - local text=string.format("Player %s AC type %s landed at dist=%.1f m (+offset=%.1f). Trapped wire=%d.", EventData.IniUnitName, _type, dist, self.carrierparam.wireoffset, wire) - text=text..string.format("X=%.1f m, Z=%.1f m, rho=%.1f m, phi=%.1f deg.", X, Z, rho, phi) - self:T(self.lid..text) - - -- We did land. - playerData.landed=true - - -- Unkonwn step until we now more. - playerData.step=AIRBOSS.PatternStep.UNDEFINED - - -- Call trapped function in 3 seconds to make sure we did not bolter. - SCHEDULER:New(self, self._Trapped, {playerData}, 1) - + else + -- Player did not land in carrier box zone. Maybe in the water near the carrier. end else @@ -5315,7 +5329,7 @@ end function AIRBOSS:_Groove(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi = self:_GetDistances(playerData.unit) + local X, Z = self:_GetDistances(playerData.unit) -- Player altitude local alt=playerData.unit:GetAltitude() @@ -5328,7 +5342,13 @@ function AIRBOSS:_Groove(playerData) self:_AbortPattern(playerData, X, Z, self.Groove, true) return end - + + -- Stern position at the rundown. + local stern=self:_GetSternCoord() + + -- Distance from rundown to player aircraft. + local rho=stern:Get2DDistance(playerData.unit:GetCoordinate()) + -- Lineup with runway centerline. local lineupError=self:_Lineup(playerData.unit, true) @@ -5451,42 +5471,55 @@ function AIRBOSS:_Groove(playerData) end -- Time since last LSO call. - local time=timer.getTime() - local deltaT=time-playerData.Tlso + local deltaT=timer.getTime()-playerData.Tlso - -- Check if we are beween 3/4 NM and end of ship. + -- Check if we are beween 3/4 NM and end of ship. Only one call every 3 seconds. if X<0 and rho>=RAR and rho=3 and playerData.waveoff==false then -- LSO call if necessary. self:_LSOadvice(playerData, glideslopeError, lineupError) + + end + + -------------------------------------------------------- + --- Some time here the landing event MIGHT be triggered. + -------------------------------------------------------- - elseif X>100 then - - if playerData.landed then - - -- Add to debrief. - if playerData.waveoff then + -- Player infront of the carrier X>~77 m. + if X>self.carrierparam.totlenght+self.carrierparam.sterndist then + + if playerData.waveoff then + + if playerData.landed then + -- This should not happen because landing event was triggered. self:_AddToDebrief(playerData, "You were waved off but landed anyway. Airboss wants to talk to you!") else - self:_AddToDebrief(playerData, "You boltered.") + self:_AddToDebrief(playerData, "You were waved off.") end - + + elseif playerData.boltered then + + -- This should not happen because landing event was triggered. + self:_AddToDebrief(playerData, "You boltered.") + else - -- Add to debrief. - self:_AddToDebrief(playerData, "You were waved off.") + -- What? Player was not waved off but flew past the carrier without landing. Why did waveoff not kick in? - -- Next step: debrief. - playerData.step=AIRBOSS.PatternStep.DEBRIEF - playerData.warning=nil end - end + + -- Next step: debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil + + end + end --- LSO check if player needs to wave off. -- Wave off conditions are: -- --- * Glide slope error > 3 degrees. +-- * Glide slope error > 1 degree. -- * Line up error > 3 degrees. -- * AoA check but only for TOPGUN graduates. -- @param #AIRBOSS self @@ -5542,6 +5575,9 @@ function AIRBOSS:_GetSternCoord() -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. local stern=self:GetCoordinate():Translate(self.carrierparam.sterndist, hdg):Translate(7, FB+90) + + -- Set altitude. + stern:SetAltitude(self.carrierparam.deckheight) return stern end @@ -5549,13 +5585,10 @@ end --- Get wire from landing position. -- @param #AIRBOSS self -- @param Core.Point#COORDINATE Lcoord Landing position. --- @param #number dc Distance correction. +-- @param #number dc Distance correction. Shift the landing coord back if dc>0 and forward if dc<0. -- @return #number Trapped wire (1-4) or 99 if no wire was trapped. function AIRBOSS:_GetWire(Lcoord, dc) - -- Heading of carrier (true). - local hdg=self.carrier:GetHeading() - -- Final bearing (true). local FB=self:GetFinalBearing() @@ -5639,40 +5672,47 @@ function AIRBOSS:_Trapped(playerData) -- Lets see if we can get a good wire. local unit=playerData.unit + -- Coordinate of player aircraft. local coord=unit:GetCoordinate() -- Get velocity in km/h. We need to substrackt the carrier velocity. local v=unit:GetVelocityKMH()-self.carrier:GetVelocityKMH() - -- Distance - local d=self:GetCoordinate():Get2DDistance(coord) - -- Stern coordinate. local stern=self:_GetSternCoord() -- Distance to stern pos. local s=stern:Get2DDistance(coord) + + -- Get current wire (estimate). This now based on the position where the player comes to a standstill which should reflect the trapped wire better. + -- TODO: Need to find the correction factor! + local dcorr=100 + local wire=self:_GetWire(coord, dcorr) -- Debug. - local text=string.format("Player %s _Trapped v=%.1f km/h, d=%.1f m, s=%.1f", playerData.name, v, d, s) + local text=string.format("Player %s _Trapped: v=%.1f km/h, s=%.1f m ==> wire=%d (dcorr=%d)", playerData.name, v, s, wire, dcorr) self:E(self.lid..text) - -- TODO: call this function again until v < threshold. Player comes to a standstill ==> Get wire! + -- Call this function again until v < threshold. Player comes to a standstill ==> Get wire! if v>5 then SCHEDULER:New(self, self._Trapped, {playerData}, 0.1) return end - + + ---------------------------------------- + --- Form this point on we have converged + ---------------------------------------- + -- Put some smoke and a mark --if self.Debug then coord:SmokeBlue() coord:MarkToAll(text) stern:MarkToAll("Stern") --end - - -- Get wire. - local wire=playerData.wire + -- Set player wire. + playerData.wire=wire + -- Message to player. local text=string.format("Trapped %d-wire.", wire) if wire==3 then @@ -5694,10 +5734,10 @@ function AIRBOSS:_Trapped(playerData) else - --Still in air ==> Boltered! - local text="Player boltered in trapped function." + --Again in air ==> Boltered! + local text=string.format("Player %s boltered in trapped function.", playerData.name) self:T(self.lid..text) - MESSAGE:New("Player boltered in trapped", 5, "DEBUG"):ToAllIf(self.debug) + MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.debug) -- Bolter switch on. playerData.boltered=true @@ -5931,6 +5971,77 @@ function AIRBOSS:_GetZoneCorridor(case) return zone end + +--- Get zone of carrier. Carrier is approximated as rectangle. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE Zone surrounding the carrier. +function AIRBOSS:_GetZoneCarrierBox() + + -- Stern coordinate. + local S=self:_GetSternCoord() + + -- Current carrier heading. + local hdg=self:GetHeading(false) + + -- Coordinate array. + local p={} + + -- Starboard stern point. + p[1]=S:Translate(self.carrierparam.totwidthstarboard, hdg+90) + + -- Starboard bow point. + p[2]=p[1]:Translate(self.carrierparam.totlenght, hdg) + + -- Port bow point. + p[3]=p[2]:Translate(self.carrierparam.totwidthstarboard+self.carrierparam.totwidthport, hdg-90) + + -- Port stern point. + p[4]=p[3]:Translate(self.carrierparam.totlegth, hdg-180) + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + local zone=ZONE_POLYGON_BASE:New("Carrier Box Zone", vec2) + + return zone +end + +--- Get zone of landing runway +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE Zone surrounding landing runway. +function AIRBOSS:_GetZoneRunwayBox() + + -- Stern coordinate. + local S=self:_GetSternCoord() + + -- Current carrier heading. + local FB=self:GetFinalBearing(false) + + -- Coordinate array. + local p={} + + -- Points. + p[1]=S:Translate(self.carrierparam.rwywidth, FB+90) + p[2]=p[1]:Translate(self.carrierparam.rwylength, FB) + p[3]=p[2]:Translate(self.carrierparam.rwywidth*2, FB-90) + p[4]=p[3]:Translate(self.carrierparam.rwylength, FB-180) + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + local zone=ZONE_POLYGON_BASE:New("Landing Runway Zone", vec2) + + return zone +end + --- Get holding zone of player. -- @param #AIRBOSS self -- @param #number case Recovery case. @@ -6047,17 +6158,16 @@ end --- Get glide slope of aircraft unit. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Aircraft unit. --- @param #number optangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope. +-- @param #number optangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope ~3.5 degrees. -- @return #number Glide slope angle in degrees measured from the deck of the carrier and third wire. function AIRBOSS:_Glideslope(unit, optangle) -- Default is 0. optangle=optangle or 0 + --[[ -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. local h=unit:GetAltitude()-self.carrierparam.deckheight - - --[[ -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi = self:_GetDistances(unit) -- Distance correction. @@ -6070,12 +6180,15 @@ function AIRBOSS:_Glideslope(unit, optangle) -- Ideally we want to land at the 3-wire (or slightly before). if self.carrierparam.wire3 then - stern:Translate(self.carrierparam.wire3, self:GetFinalBearing(false)) + stern:Translate(self.carrierparam.wire3, self:GetFinalBearing(false), true) end -- Distance from stern to aircraft. local x=unit:GetCoordinate():Get2DDistance(stern) + -- Altitude of unit. Stern coordinate already includes the deck height so no correction nedded any more. + local h=unit:GetAltitude() + -- Glide slope. local glideslope=math.atan(h/x) @@ -6087,10 +6200,11 @@ end -- @param Wrapper.Unit#UNIT unit Aircraft unit. -- @param #boolean runway If true, include angled runway. -- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. --- @return #number Distance from carrier tail to player aircraft in meters. function AIRBOSS:_Lineup(unit, runway) - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + --[[ + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z, rho, phi = self:_GetDistances(unit) -- Position at the end of the deck. From there we calculate the angle. @@ -6102,7 +6216,6 @@ function AIRBOSS:_Lineup(unit, runway) -- Vector from plane to ref point on boad. local c={x=b.x-a.x, y=0, z=b.z-a.z} - --[[ -- Stern coordinate. local stern=self:_GetSternCoord() @@ -6112,7 +6225,6 @@ function AIRBOSS:_Lineup(unit, runway) -- Vector from stern to aircraft. local c={x=stern.x-coord.x, y=0, z=stern.z-coord.z} - ]] -- Current line up and error wrt to final heading of the runway. local lineup=math.deg(math.atan2(c.z, c.x)) @@ -6122,9 +6234,35 @@ function AIRBOSS:_Lineup(unit, runway) lineup=lineup-self.carrierparam.rwyangle end + ]] + + --- New stuff + + -- Carrier Orientation. + local X=COORDINATE:NewFromVec3(self.carrier:GetOrientationX()) + + -- Rotate orientation to angled runway. + if runway then + X=X:Rotate2D(self.carrierparams.rwyangle) + end + + -- Stern coordinate. + local S=self:_GetSternCoord() + S.y=0 -- 2D only + + -- Plane coordinate. + local P=unit:GetCoordinate() + P.y=0 -- 2D only + + -- Vector from Plane to Stern V=S-P + local V=UTILS.VecSubstract(S, P) + + -- Angle between carrier orientation and + local alpha=UTILS.VecAngle(X,V) + env.info("FF lineup = "..lineup) - return lineup, UTILS.VecNorm(c) + return lineup end --- Get true (or magnetic) heading of carrier. @@ -6306,7 +6444,7 @@ function AIRBOSS:_GetDistances(unit) -- Orientation of carrier. local z=self.carrier:GetOrientationZ() - -- Projection of player pos on z component. + -- Projection of player pos on z component. local dz=UTILS.VecDot(z,c) -- Polar coordinates diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 487212d03..879680676 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -716,6 +716,23 @@ function UTILS.VecCross(a, b) return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} end +--- Calculate the difference between two 3D vectors by substracting the x,y,z components from each other. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return DCS#Vec3 Vector c=a-b with c(i)=a(i)-b(i), i=x,y,z. +function UTILS.VecSubstract(a, b) + return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} +end + +--- Calculate the angle between two 3D vectors. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return #number Angle alpha between and b in degrees. alpha=acos(a*b)/(|a||b|), (* denotes the dot product). +function UTILS.VecAngle(a, b) + local alpha=math.acos(UTILS.VecDot(a,b)/(UTILS.VecNorm(a)*UTILS.VecNorm(b))) + return math.deg(alpha) +end + --- Converts a TACAN Channel/Mode couple into a frequency in Hz. -- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". -- @param #string TACANMode The TACAN mode, i.e. the "X" in "10X". From 6f2de65b64e31c0b68c60bf8de0a1cc394a4517e Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 22 Dec 2018 09:27:51 +0100 Subject: [PATCH 115/147] AIRBOSS v0.6.0 --- Moose Development/Moose/Core/Zone.lua | 10 +- Moose Development/Moose/Ops/Airboss.lua | 159 +++++++++--------- .../Moose/Ops/RecoveryTanker.lua | 8 +- Moose Development/Moose/Ops/RescueHelo.lua | 2 +- Moose Development/Moose/Utilities/Utils.lua | 22 +++ 5 files changed, 113 insertions(+), 88 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index e0971ee06..2c00f48f5 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -535,7 +535,7 @@ function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth, AddHeight ) local Vec2 = self:GetVec2() AddHeight = AddHeight or 0 - + Points = Points and Points or 360 local Angle @@ -1431,12 +1431,16 @@ end -- @param #ZONE_POLYGON_BASE self -- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. -- @param #number Segments (Optional) Number of segments within boundary line. Default 10. +-- @param DCS#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. +-- @param #number AddHeight (optional) The height to be added for the smoke. -- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments ) +function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments, Azimuth, AddHeight ) self:F2(FlareColor) Segments=Segments or 10 + AddHeight = AddHeight or 0 + local i=1 local j=#self._.Polygon @@ -1449,7 +1453,7 @@ function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments ) for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) - POINT_VEC2:New( PointX, PointY ):Flare(FlareColor) + POINT_VEC2:New( PointX, PointY, AddHeight ):Flare(FlareColor, Azimuth) end j = i i = i + 1 diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index cf12e05c8..cbe9403d2 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -600,7 +600,7 @@ AIRBOSS.CarrierType={ -- @field #number wire4 Distance in meters from carrier position to fourth wire. -- @field #number rwylength Length of the landing runway in meters. -- @field #number rwywidth Width of the landing runway in meters. --- @field #number totlenght Total length of carrier. +-- @field #number totlength Total length of carrier. -- @field #number totwidthstarboard Total with of the carrier from stern position to starboard side (asymmetric carriers). -- @field #number totwidthport Total with of the carrier from stern position to port side (asymmetric carriers). @@ -1128,7 +1128,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.5.9w" +AIRBOSS.version="0.6.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1203,11 +1203,13 @@ function AIRBOSS:New(carriername, alias) return nil end + --[[ self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) - + ]] + -- Set some string id for output to DCS.log file. self.lid=string.format("AIRBOSS %s | ", carriername) @@ -1223,8 +1225,10 @@ function AIRBOSS:New(carriername, alias) -- Create carrier beacon. self.beacon=BEACON:New(self.carrier) - -- Defaults: - + ------------- + --- Defaults: + ------------- + -- Set up Airboss radio. self.MarshalRadio=RADIO:New(self.carrier) self.MarshalRadio:SetAlias("MARSHAL") @@ -1312,7 +1316,7 @@ function AIRBOSS:New(carriername, alias) local stern=self:_GetSternCoord() -- Bow pos. - local bow=stern:Translate(self.carrierparam.totlenght, hdg) + local bow=stern:Translate(self.carrierparam.totlength, hdg) -- End of rwy. local rwy=stern:Translate(self.carrierparam.rwylength, FB, true) @@ -1339,11 +1343,11 @@ function AIRBOSS:New(carriername, alias) rwy:FlareRed() -- Right 30 meters from stern. - local cR=stern:Translate(self.carrierparam.totstarboard, hdg+90) + local cR=stern:Translate(self.carrierparam.totwidthstarboard, hdg+90) cR:FlareYellow() -- Left 40 meters from stern. - local cL=stern:Translate(self.carrierparam.totport, hdg-90) + local cL=stern:Translate(self.carrierparam.totwidthport, hdg-90) cL:FlareYellow() --[[ @@ -1356,9 +1360,15 @@ function AIRBOSS:New(carriername, alias) w3:FlareWhite() w4:FlareYellow() ]] - + + + local cbox=self:_GetZoneCarrierBox() + local rbox=self:_GetZoneRunwayBox() + cbox:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) + rbox:FlareZone(FLARECOLOR.White, 5, nil, self.carrierparam.deckheight) end + SCHEDULER:New(nil, flareme, {}, 1, 1) end @@ -2500,7 +2510,7 @@ function AIRBOSS:_InitStennis() self.carrierparam.deckheight = 19 -- Total size of the carrier (approx as rectangle). - self.carrierparam.totlenght=310 -- Wiki says 332.8 meters overall length. + self.carrierparam.totlength=310 -- Wiki says 332.8 meters overall length. self.carrierparam.totwidthport=40 -- Wiki says 76.8 meters overall beam. self.carrierparam.totwidthstarboard=30 @@ -5486,7 +5496,7 @@ function AIRBOSS:_Groove(playerData) -------------------------------------------------------- -- Player infront of the carrier X>~77 m. - if X>self.carrierparam.totlenght+self.carrierparam.sterndist then + if X>self.carrierparam.totlength+self.carrierparam.sterndist then if playerData.waveoff then @@ -5990,13 +6000,13 @@ function AIRBOSS:_GetZoneCarrierBox() p[1]=S:Translate(self.carrierparam.totwidthstarboard, hdg+90) -- Starboard bow point. - p[2]=p[1]:Translate(self.carrierparam.totlenght, hdg) + p[2]=p[1]:Translate(self.carrierparam.totlength, hdg) -- Port bow point. p[3]=p[2]:Translate(self.carrierparam.totwidthstarboard+self.carrierparam.totwidthport, hdg-90) -- Port stern point. - p[4]=p[3]:Translate(self.carrierparam.totlegth, hdg-180) + p[4]=p[3]:Translate(self.carrierparam.totlength, hdg-180) -- Convert to vec2. local vec2={} @@ -6164,16 +6174,6 @@ function AIRBOSS:_Glideslope(unit, optangle) -- Default is 0. optangle=optangle or 0 - - --[[ - -- Glideslope. Wee need to correct for the height of the deck. The ideal glide slope is 3.5 degrees. - local h=unit:GetAltitude()-self.carrierparam.deckheight - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi = self:_GetDistances(unit) - -- Distance correction. - local offx=self.carrierparam.wire3 or self.carrierparam.sterndist - local x=math.abs(self.carrierparam.wire3-X) - ]] -- Stern coordinate. local stern=self:_GetSternCoord() @@ -6190,9 +6190,12 @@ function AIRBOSS:_Glideslope(unit, optangle) local h=unit:GetAltitude() -- Glide slope. - local glideslope=math.atan(h/x) + local glideslope=math.atan(h/x) + + -- Glide slope (error) in degrees. + local gs=math.deg(glideslope)-optangle - return math.deg(glideslope)-optangle + return gs end --- Get line up of player wrt to carrier. @@ -6200,69 +6203,59 @@ end -- @param Wrapper.Unit#UNIT unit Aircraft unit. -- @param #boolean runway If true, include angled runway. -- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. -function AIRBOSS:_Lineup(unit, runway) - - --[[ - - -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) - local X, Z, rho, phi = self:_GetDistances(unit) +function AIRBOSS:_Lineup(unit, runway) - -- Position at the end of the deck. From there we calculate the angle. - local b={x=self.carrierparam.sterndist, z=0} + -- Vector to carrier. + local A=self:_GetSternCoord():GetVec3() - -- Position of the aircraft wrt carrier coordinates. - local a={x=X, z=Z} - - -- Vector from plane to ref point on boad. - local c={x=b.x-a.x, y=0, z=b.z-a.z} - - -- Stern coordinate. - local stern=self:_GetSternCoord() + -- Vector to player. + local B=unit:GetVec3() - -- Position of aircraft. - local coord=unit:GetCoordinate() + -- Vector from player to carrier. + local C=UTILS.VecSubstract(A, B) - -- Vector from stern to aircraft. - local c={x=stern.x-coord.x, y=0, z=stern.z-coord.z} + -- Only in 2D plane. + C.y=0 - - -- Current line up and error wrt to final heading of the runway. - local lineup=math.deg(math.atan2(c.z, c.x)) - - -- Include runway. - if runway then - lineup=lineup-self.carrierparam.rwyangle - end - - ]] - - --- New stuff - - -- Carrier Orientation. - local X=COORDINATE:NewFromVec3(self.carrier:GetOrientationX()) + -- Orientation of carrier. + local X=self.carrier:GetOrientationX() -- Rotate orientation to angled runway. if runway then - X=X:Rotate2D(self.carrierparams.rwyangle) + X=UTILS.Rotate2D(X, -self.carrierparam.rwyangle) end - -- Stern coordinate. - local S=self:_GetSternCoord() - S.y=0 -- 2D only + -- Projection of player pos on x component. + local x=UTILS.VecDot(X, C) - -- Plane coordinate. - local P=unit:GetCoordinate() - P.y=0 -- 2D only + -- Orientation of carrier. + local Z=self.carrier:GetOrientationZ() - -- Vector from Plane to Stern V=S-P - local V=UTILS.VecSubstract(S, P) + -- Rotate orientation to angled runway. + if runway then + Z=UTILS.Rotate2D(Z, -self.carrierparam.rwyangle) + end - -- Angle between carrier orientation and - local alpha=UTILS.VecAngle(X,V) + -- Projection of player pos on z component. + local z=UTILS.VecDot(Z, C) - env.info("FF lineup = "..lineup) + --- - return lineup + -- Position of the aircraft in the new coordinate system. + local a={x=x, y=0, z=z} + + -- Stern position in the new coordinate system, which is simply the origin. + local b={x=0, y=0, z=0} + + -- Vector from plane to ref point on the boat. + local c=UTILS.VecSubstract(a, b) + + -- Current line up and error wrt to final heading of the runway. + local lineup=math.deg(math.atan2(c.z, c.x)) + + --env.info(string.format("FF lineup 2 = %.1f", lineup)) + + return lineup end --- Get true (or magnetic) heading of carrier. @@ -7228,14 +7221,19 @@ function AIRBOSS:_Debrief(playerData) -- LSO grade: (OK) 3.0 PT - LURIM local text=string.format("%s %.1f PT - %s", grade, points, analysis) - -- Wire trapped. Not if pattern WI. - if playerData.wire and not playerData.patternwo then - text=text..string.format(" %d-wire", playerData.wire) - end + -- Wire and Groove time only if not pattern WO. + if not playerData.patternwo then - -- Time in the groove. Only Case I/II and not pattern WO. - if playerData.Tgroove and playerData.case<3 and not playerData.patternwo then - text=text..string.format("\nYour detailed debriefing can be found via the F10 radio menu.") + -- Wire trapped. Not if pattern WI. + if playerData.wire then + text=text..string.format(" %d-wire", playerData.wire) + end + + -- Time in the groove. Only Case I/II and not pattern WO. + if playerData.Tgroove and playerData.Tgroovey<=60 and playerData.case<3 then + text=text..string.format("\nTime in the groove %d seconds.", playerData.Tgroove) + end + end -- Info text. @@ -7250,6 +7248,7 @@ function AIRBOSS:_Debrief(playerData) -- Set step to undefined and check. playerData.step=AIRBOSS.PatternStep.UNDEFINED + -- Check what happened? if playerData.patternwo then diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 93d171680..929db1163 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -701,7 +701,7 @@ function RECOVERYTANKER:onafterStart(From, Event, To) local dist=-self.distStern+UTILS.NMToMeters(4) -- Coordinate behind the carrier and slightly port. - local Carrier=self.carrier:GetCoordinate():SetAltitude(self.altitude):Translate(dist, hdg+190) + local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg+190):SetAltitude(self.altitude) -- Orientation of spawned group. Spawn:InitHeading(hdg+10) @@ -853,11 +853,11 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) local Carrier=self.carrier:GetCoordinate() -- Define race-track pattern. - local p0=self.tanker:GetCoordinate():Translate(UTILS.NMToMeters(1), self.tanker:GetHeading()) + local p0=self.tanker:GetCoordinate():Translate(UTILS.NMToMeters(1), self.tanker:GetHeading(), true) -- Racetrack pattern points. - local p1=Carrier:SetAltitude(self.altitude):Translate(self.distStern, hdg) - local p2=Carrier:SetAltitude(self.altitude):Translate(self.distBow, hdg) + local p1=Carrier:Translate(self.distStern, hdg):SetAltitude(self.altitude) + local p2=Carrier:Translate(self.distBow, hdg):SetAltitude(self.altitude) -- Set orbit task. local taskorbit=self.tanker:TaskOrbit(p1, self.altitude, self.speed, p2) diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 2d229a1ef..dca4caab2 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -782,7 +782,7 @@ function RESCUEHELO:onafterStart(From, Event, To) local dist=UTILS.NMToMeters(0.2) -- Coordinate behind the carrier. Altitude at least 100 meters for spawning because it drops down a bit. - local Carrier=self.carrier:GetCoordinate():SetAltitude(math.max(140, self.altitude)):Translate(dist, hdg) + local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg):SetAltitude(math.max(140, self.altitude)) -- Orientation of spawned group. Spawn:InitHeading(hdg) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 879680676..ca20c1b7f 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -733,6 +733,28 @@ function UTILS.VecAngle(a, b) return math.deg(alpha) end +--- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec3 Vector rotated in the (x,z) plane. +function UTILS.Rotate2D(a, angle) + + local phi=math.rad(angle) + + local x=a.z + local y=a.x + + local Z=x*math.cos(phi)-y*math.sin(phi) + local X=x*math.sin(phi)+y*math.cos(phi) + local Y=a.y + + local A={x=X, y=Y, z=Z} + + return A +end + + + --- Converts a TACAN Channel/Mode couple into a frequency in Hz. -- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". -- @param #string TACANMode The TACAN mode, i.e. the "X" in "10X". From 49e7a24e2874cb8c13ff064c24a6563699bcff84 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 22 Dec 2018 15:55:43 +0100 Subject: [PATCH 116/147] AIRBOSS v0.6.1 Lineup, glide slope fixes. --- Moose Development/Moose/Ops/Airboss.lua | 64 +++++++++++++++---------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index cbe9403d2..6ca466bc3 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -11,7 +11,7 @@ -- * Define recovery time windows with individual recovery cases. -- * Automatic TACAN and ICLS channel setting of carrier. -- * Separate radio channels for LSO and Marshal transmissions. --- * Voice over support for LSO and Marshal radio transmissions with more than 30 common radio calls. +-- * Voice over support for LSO and Marshal radio transmissions. -- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, -- help function (player aircraft attitude, marking of pattern zones etc). -- * Recovery tanker and refueling option via integration of @{Ops.RecoveryTanker} class. @@ -436,7 +436,7 @@ -- -- The @{#AIRBOSS} class allows to handle incoming AI units and integrate them into the marshal and landing pattern. -- --- By default, incoming carrier capable aircraft which are detecting inside the CCZ and approach the carrier by more than 5 NM are automatically guided to the holding zone. +-- By default, incoming carrier capable aircraft which are detecting inside the Carrier Controlled Area (CCA) and approach the carrier by more than 5 NM are automatically guided to the holding zone. -- Each AI group gets its own marshal stack in the holding pattern. Once a recovery window opens, the AI group of the lowest stack is transitioning to the landing pattern -- and the Marshal stack collapses. -- @@ -1128,22 +1128,23 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.6.0" +AIRBOSS.version="0.6.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Improve radio messages. Maybe usersound for messages which are only meant for players? -- TODO: Player eject and crash debrief "gradings". --- DONE: Add voice over fly needs and welcome aboard. --- TODO: Improve trapped wire calculation. --- DONE: Carrier zone with dimensions of carrier. to check if landing happend on deck. --- DONE: Carrier runway zone for fould deck check. -- TODO: Subtitles off options on player level. -- TODO: PWO during case 2/3. Also when too close to other player. -- TODO: Option to filter AI groups for recovery. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! -- TODO: Persistence of results. +-- DONE: Add voice over fly needs and welcome aboard. +-- DONE: Improve trapped wire calculation. +-- DONE: Carrier zone with dimensions of carrier. to check if landing happend on deck. +-- DONE: Carrier runway zone for fould deck check. -- DONE: More Hints for Case II/III. -- DONE: Set magnetic declination function. -- DONE: First send AI to marshal and then allow them into the landing pattern ==> task function when reaching the waypoint. @@ -2004,7 +2005,7 @@ function AIRBOSS:_CheckAIStatus() -- Pilot: "405, Hornet Ball, 3.2" -- TODO: Voice over. - local text=string.format("%s Ball, %.1f.", self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) + local text=string.format("%s Ball, %.1f.", self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) self:MessageToPattern(text, element.onboard, "", 3, false, 0, true) -- Debug message. @@ -4382,7 +4383,7 @@ function AIRBOSS:OnEventLand(EventData) end -- Get wire. We additionally shift the landing coord back because landing event for players is unfortunately delayed. - local wire=self:_GetWire(coord, 65) + local wire=self:_GetWire(coord, 100) -- No wire ==> Bolter, Bolter radio call. -- TODO: might need a better place for this. or check @@ -5368,6 +5369,9 @@ function AIRBOSS:_Groove(playerData) -- Get AoA. local AoA=playerData.unit:GetAoA() + -- For debugging. + --MESSAGE:New(string.format("LUE=%.1f GLE=%.1f AoA=%.1f", lineupError, glideslopeError, AoA), 3, nil, true):ToAll() + -- Ranges in the groove. local RXX=UTILS.NMToMeters(0.750)+math.abs(self.carrierparam.sterndist) -- Start of groove. 0.75 = 1389 m local RRB=UTILS.NMToMeters(0.500)+math.abs(self.carrierparam.sterndist) -- Roger Ball! call. 0.5 = 926 m @@ -5545,13 +5549,17 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) -- Too high or too low? if math.abs(glideslopeError)>1 then - self:T(self.lid..string.format("%s: Wave off due to glide slope error |%.1f| > 1 degree!", playerData.name, glideslopeError)) + local text=string.format("Wave off due to glide slope error |%.1f| > 1 degree!", glideslopeError) + self:I(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) waveoff=true end -- Too far from centerline? if math.abs(lineupError)>3 then - self:T(self.lid..string.format("%s: Wave off due to line up error |%.1f| > 3 degrees!", playerData.name, lineupError)) + local text=string.format("Wave off due to line up error |%.1f| > 3 degrees!", lineupError) + self:I(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) waveoff=true end @@ -5561,10 +5569,14 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) local aoaac=self:_GetAircraftAoA(playerData) -- Check too slow or too fast. if AoAaoaac.Slow then - self:T(self.lid..string.format("%s: Wave off due to AoA %.1f > %.1f!", playerData.name, AoA, aoaac.Slow)) + local text=string.format("Wave off due to AoA %.1f > %.1f!", AoA, aoaac.Slow) + self:I(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) waveoff=true end end @@ -5695,7 +5707,6 @@ function AIRBOSS:_Trapped(playerData) local s=stern:Get2DDistance(coord) -- Get current wire (estimate). This now based on the position where the player comes to a standstill which should reflect the trapped wire better. - -- TODO: Need to find the correction factor! local dcorr=100 local wire=self:_GetWire(coord, dcorr) @@ -5713,12 +5724,12 @@ function AIRBOSS:_Trapped(playerData) --- Form this point on we have converged ---------------------------------------- - -- Put some smoke and a mark - --if self.Debug then + -- Put some smoke and a mark. + if self.Debug then coord:SmokeBlue() coord:MarkToAll(text) stern:MarkToAll("Stern") - --end + end -- Set player wire. playerData.wire=wire @@ -6178,23 +6189,26 @@ function AIRBOSS:_Glideslope(unit, optangle) -- Stern coordinate. local stern=self:_GetSternCoord() - -- Ideally we want to land at the 3-wire (or slightly before). + -- Ideally we want to land between 2nd and 3rd wire. if self.carrierparam.wire3 then - stern:Translate(self.carrierparam.wire3, self:GetFinalBearing(false), true) + local d23=self.carrierparam.wire2+0.5*(self.carrierparam.wire3-self.carrierparam.wire2) + stern=stern:Translate(d23, self:GetFinalBearing(false), true) end - + -- Distance from stern to aircraft. local x=unit:GetCoordinate():Get2DDistance(stern) - -- Altitude of unit. Stern coordinate already includes the deck height so no correction nedded any more. - local h=unit:GetAltitude() + -- Altitude of unit corrected by the deck height of the carrier. + local h=unit:GetAltitude()-self.carrierparam.deckheight -- Glide slope. local glideslope=math.atan(h/x) -- Glide slope (error) in degrees. local gs=math.deg(glideslope)-optangle - + + --env.info(string.format("FF Glide slope error = %.1f, x=%.1f h=%.1f", gs, x, h)) + return gs end @@ -7230,7 +7244,7 @@ function AIRBOSS:_Debrief(playerData) end -- Time in the groove. Only Case I/II and not pattern WO. - if playerData.Tgroove and playerData.Tgroovey<=60 and playerData.case<3 then + if playerData.Tgroove and playerData.Tgroove<=60 and playerData.case<3 then text=text..string.format("\nTime in the groove %d seconds.", playerData.Tgroove) end @@ -8085,8 +8099,6 @@ function AIRBOSS:_Number2Sound(radio, number, delay) sender="LSOCall" elseif alias=="MARSHAL" then sender="MarshalCall" - --elseif alias=="AIRBOSS" then - -- sender="AirbossCall" else self:E(self.lid.."ERROR: Unknown radio alias!") end From d8c5ab7eaeb53875611486d7a7311ddd67a31ee5 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 23 Dec 2018 01:30:33 +0100 Subject: [PATCH 117/147] AIRBOSS v0.6.2 --- Moose Development/Moose/Ops/Airboss.lua | 211 ++++++++++++++++-------- 1 file changed, 138 insertions(+), 73 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 6ca466bc3..52aecc16c 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1109,6 +1109,7 @@ AIRBOSS.GroovePos={ -- @field #number passes Number of passes. -- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. -- @field #table debrief Debrief analysis of the current step of this pass. +-- @field #table lastdebrief Debrief of player performance of last completed pass. -- @field #table grades LSO grades of player passes. -- @field #boolean landed If true, player landed or attempted to land. -- @field #boolean boltered If true, player boltered. @@ -1128,13 +1129,15 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.6.1" +AIRBOSS.version="0.6.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Improve radio messages. Maybe usersound for messages which are only meant for players? +-- TODO: Include recovery tanker into next stack calculation. Angels six should be empty. +-- TODO: Get charly time estimate function. -- TODO: Player eject and crash debrief "gradings". -- TODO: Subtitles off options on player level. -- TODO: PWO during case 2/3. Also when too close to other player. @@ -1204,13 +1207,13 @@ function AIRBOSS:New(carriername, alias) return nil end - --[[ - self.Debug=true - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) - ]] - + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + -- Set some string id for output to DCS.log file. self.lid=string.format("AIRBOSS %s | ", carriername) @@ -1307,7 +1310,7 @@ function AIRBOSS:New(carriername, alias) self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) end - -- Carrier parameter tests. + -- Carrier parameter debug tests. if false then -- Stern coordinate. local FB=self:GetFinalBearing(false) @@ -1322,7 +1325,7 @@ function AIRBOSS:New(carriername, alias) -- End of rwy. local rwy=stern:Translate(self.carrierparam.rwylength, FB, true) - + --- Flare points and zones. local function flareme() -- Carrier pos. @@ -1350,28 +1353,26 @@ function AIRBOSS:New(carriername, alias) -- Left 40 meters from stern. local cL=stern:Translate(self.carrierparam.totwidthport, hdg-90) cL:FlareYellow() - - --[[ - local w1=stern:Translate(46, FB) - local w2=stern:Translate(46+12, FB) - local w3=stern:Translate(46+24, FB) - local w4=stern:Translate(46+35, FB) + + -- Flare wires. + local w1=stern:Translate(self.carrierparam.wire1, FB) + local w2=stern:Translate(self.carrierparam.wire2, FB) + local w3=stern:Translate(self.carrierparam.wire3, FB) + local w4=stern:Translate(self.carrierparam.wire4, FB) w1:FlareWhite() w2:FlareYellow() w3:FlareWhite() w4:FlareYellow() - ]] - + -- Flare carrier and landing runway. local cbox=self:_GetZoneCarrierBox() local rbox=self:_GetZoneRunwayBox() cbox:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) rbox:FlareZone(FLARECOLOR.White, 5, nil, self.carrierparam.deckheight) end - - SCHEDULER:New(nil, flareme, {}, 1, 1) - + -- Flare points every 3 seconds for 3 minutes. + SCHEDULER:New(nil, flareme, {}, 1, 3, nil, 180) end -- If calls should be part of self and individual for different carriers. @@ -3151,7 +3152,7 @@ function AIRBOSS:_MarshalAI(flight, nstack) local pE=Carrier:Translate(UTILS.NMToMeters(7), hdg-30):SetAltitude(altitude) -- Entry point 5 NM port and slightly astern the boat. - p0=Carrier:Translate(UTILS.NMToMeters(5*math.sqrt(2)), hdg-135):SetAltitude(altitude) + p0=Carrier:Translate(UTILS.NMToMeters(5), hdg-135):SetAltitude(altitude) -- Waypoint ahead of carrier's holding zone. wp[#wp+1]=pE:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") @@ -3213,10 +3214,23 @@ end function AIRBOSS:_LandAI(flight) -- Debug info. - self:T(self.lid..string.format("Landing AI flight %s.", flight.groupname)) + self:T(self.lid..string.format("Landing AI flight %s.", flight.groupname)) + + -- NOTE: Looks like the AI needs to approach at the "correct" speed. If they are too fast, they fly an unnecessary circle to bleed of speed first. + -- Unfortunately, the correct speed depends on the aircraft type! -- Aircraft speed when flying the pattern. - local Speed=UTILS.KnotsToKmph(274) + local Speed=UTILS.KnotsToKmph(200) + + if flight.actype==AIRBOSS.AircraftCarrier.HORNET or flight.actype==AIRBOSS.AircraftCarrier.FA18C then + Speed=UTILS.KnotsToKmph(200) + elseif flight.actype==AIRBOSS.AircraftCarrier.E2D then + Speed=UTILS.KnotsToKmph(150) + elseif flight.actype==AIRBOSS.AircraftCarrier.F14A then + Speed=UTILS.KnotsToKmph(175) + elseif flight.actype==AIRBOSS.AircraftCarrier.S3B or flight.actype==AIRBOSS.AircraftCarrier.S3BTANKER then + Speed=UTILS.KnotsToKmph(140) + end -- Carrier position. local Carrier=self:GetCoordinate() @@ -3226,15 +3240,20 @@ function AIRBOSS:_LandAI(flight) -- Waypoints array. local wp={} + + local CurrentSpeed=flight.group:GetVelocityKMH() -- Current positon. - wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, Speed, {}, "Current position") + wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, CurrentSpeed, {}, "Current position") - -- Altitude 2000 ft - local alt=UTILS.FeetToMeters(2000) + -- Altitude 800 ft. Looks like this works best. + local alt=UTILS.FeetToMeters(800) -- Landing waypoint 5 NM behind carrier at 2000 ft = 610 meters ASL. - wp[#wp+1]=self:GetCoordinate():Translate(-UTILS.NMToMeters(5), hdg):SetAltitude(alt):WaypointAirLanding(Speed, self.airbase, nil, "Landing") + wp[#wp+1]=Carrier:Translate(UTILS.NMToMeters(4), hdg-160):SetAltitude(alt):WaypointAirLanding(Speed, self.airbase, nil, "Landing") + --wp[#wp+1]=self:GetCoordinate():Translate(UTILS.NMToMeters(3), hdg-160):SetAltitude(alt):WaypointAirTurningPoint(nil,Speed, {}, "Before Initial") ---WaypointAirLanding(Speed, self.airbase, nil, "Landing") + -- + --wp[#wp+1]=self:GetCoordinate():WaypointAirLanding(Speed, self.airbase, nil, "Landing") -- Reinit waypoints. flight.group:WayPointInitialize(wp) @@ -3268,6 +3287,9 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) local Dist local p1=nil --Core.Point#COORDINATE local p2=nil --Core.Point#COORDINATE + + -- Stack number. + local nstack=stack-1 if case==1 then @@ -3282,7 +3304,7 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) -- First point over carrier. p1=Carrier - -- Seconds point 1.5 NM ahead. + -- Second point 1.5 NM ahead. p2=Carrier:Translate( UTILS.NMToMeters(1.5), hdg) else @@ -3291,7 +3313,7 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) angels0=6 -- Distance: d=n*angles0+15 NM, so first stack is at 15+6=21 NM - Dist=UTILS.NMToMeters((stack-1)+angels0+15) + Dist=UTILS.NMToMeters(nstack+angels0+15) -- Get correct radial depending on recovery case including offset. local radial=self:GetRadial(case, false, true) @@ -3308,7 +3330,7 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) end -- Pattern altitude. - local altitude=UTILS.FeetToMeters(((stack-1)+angels0)*1000) + local altitude=UTILS.FeetToMeters((nstack+angels0)*1000) -- Set altitude of coordinate. p1:SetAltitude(altitude, true) @@ -3405,50 +3427,52 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- Maybe need to set the initial value to 1000? Or check stack>0 of pattern flight? if stack>0 and mstack>stack then - -- Decrease stack/flag by one ==> AI will go lower. + -- New stack is old stack minus one. -- TODO: If we include the recovery tanker, this needs to be generalized. - mflight.flag:Set(mstack-1) + local newstack=mstack-1 + + -- Debug info. + self:T(self.lid..string.format("Flight %s case %d is changing marshal stack %d --> %d.", mflight.groupname, mflight.case, mstack, newstack)) if mflight.ai then - -- Command AI to decrease stack. - self:_MarshalAI(flight, mstack-1) + -- Command AI to decrease stack. Flag is set in the routine. + self:_MarshalAI(mflight, newstack) else + + -- Decrease stack/flag. Human player needs to take care himself. + mflight.flag:Set(newstack) -- Inform players. if mflight.difficulty~=AIRBOSS.Difficulty.HARD then -- Send message to all non-pros that they can descent. - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(mstack-1, case)) + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(newstack, case)) local text=string.format("descent to next lower stack at %d ft", alt) self:MessageToPlayer(mflight, text, "MARSHAL") end - - end - - -- Debug info. - self:T(self.lid..string.format("Flight %s case %d is changing marshal stack %d --> %d.", mflight.groupname, mflight.case, mstack, mstack-1)) - - -- Loop over section members. - for _,_sec in pairs(mflight.section) do - local sec=_sec --#AIRBOSS.PlayerData - - -- Also decrease flag for section members of flight. - sec.flag:Set(mstack-1) - - -- Inform section member. - if sec.difficulty~=AIRBOSS.Difficulty.HARD then - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(mstack-1,case)) - local text=string.format("follow your lead to next lower stack at %d ft", alt) - self:MessageToPlayer(sec, text, "MARSHAL") - end - end - + + -- Loop over section members. + for _,_sec in pairs(mflight.section) do + local sec=_sec --#AIRBOSS.PlayerData + + -- Also decrease flag for section members of flight. + sec.flag:Set(newstack) + + -- Inform section member. + if sec.difficulty~=AIRBOSS.Difficulty.HARD then + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(newstack, case)) + local text=string.format("follow your lead to next lower stack at %d ft", alt) + self:MessageToPlayer(sec, text, "MARSHAL") + end + + end + + end end - - end + end end @@ -3733,6 +3757,9 @@ function AIRBOSS:_NewPlayer(unitname) -- LSO grades. playerData.grades=playerData.grades or {} + -- Debriefing tables. + playerData.lastdebrief=playerData.lastdebrief or {} + -- Attitude monitor. playerData.attitudemonitor=false @@ -5370,7 +5397,7 @@ function AIRBOSS:_Groove(playerData) local AoA=playerData.unit:GetAoA() -- For debugging. - --MESSAGE:New(string.format("LUE=%.1f GLE=%.1f AoA=%.1f", lineupError, glideslopeError, AoA), 3, nil, true):ToAll() + --MESSAGE:New(string.format("LineUp=%.1f GlideSlope=%.1f AoA=%.1f", lineupError, glideslopeError, AoA), 3, nil, true):ToAll() -- Ranges in the groove. local RXX=UTILS.NMToMeters(0.750)+math.abs(self.carrierparam.sterndist) -- Start of groove. 0.75 = 1389 m @@ -6365,7 +6392,7 @@ function AIRBOSS:GetRadial(case, magnetic, offset, inverse) elseif case==3 then -- Radial wrt angled runway. - local radial=self:GetFinalBearing(magnetic)-180 + radial=self:GetFinalBearing(magnetic)-180 -- Holding offset angle (+-15 or 30 degrees usually) if offset then @@ -6392,6 +6419,7 @@ function AIRBOSS:GetRadial(case, magnetic, offset, inverse) end + return radial end --- Get relative heading of player wrt carrier. @@ -6598,6 +6626,47 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) playerData.Tlso=timer.getTime() end +--- Grade player time in the groove - from turning to final until touchdown. +-- +-- If time +-- +-- * < 9 seconds: No Grade "--" +-- * 9-11 seconds: Fair "(OK)" +-- * 12-21 seconds: OK (15-18 is ideal) +-- * 22-24 seconds: Fair "(OK) +-- * > 24 seconds: No Grade "--" +-- +-- If you manage to be between 16.4 and and 16.6 seconds, you will even get and okay underline "\_OK\_". +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #string LSO grade for time in groove, i.e. \_OK\_, OK, (OK), --. +function AIRBOSS:_EvalGrooveTime(playerData) + + -- Time in groove. + local t=playerData.Tgroove + + local grade="" + if t<9 then + grade="--" + elseif t<12 then + grade="(OK)" + elseif t<22 then + grade="OK" + elseif t<=24 then + grade="(OK)" + else + grade="--" + end + + -- The unicorn! + if t>=16.4 and t<=16.6 then + grade="_OK_" + end + + return grade +end + --- Grade approach. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -6661,14 +6730,6 @@ function AIRBOSS:_LSOgrade(playerData) text=text.."# of normal deviations = "..nN.."\n" text=text.."# of small deviations ( = "..nS.."\n" self:T2(self.lid..text) - - --[[ - <9 seconds: No Grade - 9-11 seconds: Fair - 12-21 seconds(15-18 is ideal): OK - 22-24 seconds: Fair - >24 seconds: No Grade - ]] -- Special cases. if playerData.patternwo then @@ -7208,6 +7269,7 @@ end -- @param #string hint Debrief text of this step. -- @param #string step (Optional) Current step in the pattern. Default from playerData. function AIRBOSS:_AddToDebrief(playerData, hint, step) + playerData.debrief={} step=step or playerData.step table.insert(playerData.debrief, {step=step, hint=hint}) end @@ -7245,11 +7307,14 @@ function AIRBOSS:_Debrief(playerData) -- Time in the groove. Only Case I/II and not pattern WO. if playerData.Tgroove and playerData.Tgroove<=60 and playerData.case<3 then - text=text..string.format("\nTime in the groove %d seconds.", playerData.Tgroove) + text=text..string.format("\nTime in the groove %d seconds: %s", playerData.Tgroove, self:_EvalGrooveTime(playerData)) end end + -- Copy debriefing text. + playerData.lastdebrief=UTILS.DeepCopy(playerData.debrief) + -- Info text. if playerData.difficulty==AIRBOSS.Difficulty.EASY then text=text..string.format("\nYour detailed debriefing can be found via the F10 radio menu.") @@ -8696,9 +8761,9 @@ function AIRBOSS:_DisplayDebriefing(_unitName) local text=string.format("Debriefing:") -- Check if data is present. - if #playerData.debrief>0 then + if #playerData.lastdebrief>0 then text=text..string.format("\n================================\n") - for _,_data in pairs(playerData.debrief) do + for _,_data in pairs(playerData.lastdebrief) do local step=_data.step local comment=_data.hint text=text..string.format("* %s:\n",step) @@ -8710,7 +8775,7 @@ function AIRBOSS:_DisplayDebriefing(_unitName) -- Send debrief message to player self:MessageToPlayer(playerData, text, nil , "", 30, true) - + end end end From 8dc564259962cc48974c2c7bba6787dda05e4309 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 25 Dec 2018 18:55:35 +0100 Subject: [PATCH 118/147] AIBOSS v0.6.3 Recovery Tanker v1.0.0 Rescue Helo v1.0.0 Fixed spawn after engine shutdown bug. Added new PG airbases. --- Moose Development/Moose/Core/Spawn.lua | 4 +- Moose Development/Moose/Ops/Airboss.lua | 547 ++++++++++++------ .../Moose/Ops/RecoveryTanker.lua | 21 +- Moose Development/Moose/Ops/RescueHelo.lua | 35 +- Moose Development/Moose/Wrapper/Airbase.lua | 4 + Moose Development/Moose/Wrapper/Group.lua | 2 +- 6 files changed, 397 insertions(+), 216 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 466e6a70f..0d4156260 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2637,7 +2637,9 @@ function SPAWN:_OnEngineShutDown( EventData ) if Landed and self.RepeatOnEngineShutDown then local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) + --self:ReSpawn( SpawnGroupIndex ) + -- Delay respawn by three seconds due to DCS 2.5.4 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 + SCHEDULER:New(self, self.ReSpawn, {SpawnGroupIndex}, 3) end end end diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 52aecc16c..729e53218 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -127,6 +127,7 @@ -- @field #string defaultskill Default player skill @{#AIRBOSS.Difficulty}. -- @field #boolean adinfinitum If true, carrier patrols ad infinitum, i.e. when reaching its last waypoint it starts at waypoint one again. -- @field #number magvar Magnetic declination in degrees. +-- @field #number Tcollapse Last time timer.gettime() the stack collapsed. -- @extends Core.Fsm#FSM --- Be the boss! @@ -542,6 +543,7 @@ AIRBOSS = { defaultskill = nil, adinfinitum = nil, magvar = nil, + Tcollapse = nil, } --- Player aircraft types capable of landing on carriers. @@ -815,7 +817,7 @@ AIRBOSS.LSOCall={ file="LSO-WelcomeAboard", suffix="ogg", loud=false, - subtitle="Welcome aboard.", + subtitle="Welcome aboard", duration=0.9, }, N0={ @@ -1129,13 +1131,13 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.6.2" +AIRBOSS.version="0.6.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Improve radio messages. Maybe usersound for messages which are only meant for players? +-- TODO: Check player heading at zones, e.g. initial. -- TODO: Include recovery tanker into next stack calculation. Angels six should be empty. -- TODO: Get charly time estimate function. -- TODO: Player eject and crash debrief "gradings". @@ -1144,6 +1146,9 @@ AIRBOSS.version="0.6.2" -- TODO: Option to filter AI groups for recovery. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! -- TODO: Persistence of results. +-- DONE: Fix bug that player leaves the approach zone if he boltered or was waved off during Case II or III. NOTE: Partly due to increasing approach zone size. +-- DONE: Fix bug that player gets an altitude warning if stack collapses. NOTE: Would not work if two stacks Case I and II/III are used. +-- DONE: Improve radio messages. Maybe usersound for messages which are only meant for players? -- DONE: Add voice over fly needs and welcome aboard. -- DONE: Improve trapped wire calculation. -- DONE: Carrier zone with dimensions of carrier. to check if landing happend on deck. @@ -1207,6 +1212,7 @@ function AIRBOSS:New(carriername, alias) return nil end + -- Debug trace. if false then self.Debug=true BASE:TraceOnOff(true) @@ -1872,9 +1878,8 @@ function AIRBOSS:onafterStart(From, Event, To) self.Tqueue=timer.getTime() -- Schedule radio queue checks. - -- TODO: id's to self to be able to stop the scheduler. - local RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQLSO, "LSO"}, 1, 0.01) - local RQMid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQMarshal, "MARSHAL"}, 1, 0.01) + self.RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQLSO, "LSO"}, 1, 0.01) + self.RQMid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQMarshal, "MARSHAL"}, 1, 0.01) -- Initial carrier position and orientation. self.Cposition=self:GetCoordinate() @@ -2280,6 +2285,10 @@ function AIRBOSS:onafterRecoveryStart(From, Event, To, Case, Offset) MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) self:T(self.lid..text) + -- Message to all players in marshal stack. + -- TODO: maybe to all flights in CCA? + self:MessageToMarshal(text, "MARSHAL", "99") + -- Switch to case. self:RecoveryCase(Case, Offset) end @@ -2566,7 +2575,7 @@ function AIRBOSS:_InitStennis() self.BreakEntry.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. self.BreakEntry.Xmax= nil self.BreakEntry.Zmin=-400 -- Not more than 400 m port of boat. Otherwise miss the zone. - self.BreakEntry.Zmax=1000 -- Not more than 1000 m starboard of boat. Otherwise miss the zone. + self.BreakEntry.Zmax=UTILS.NMToMeters(1.5) -- Not more than 1.5 NM starboard. self.BreakEntry.LimitXmin=0 -- Check and next step when at carrier and starboard of carrier. self.BreakEntry.LimitXmax=nil self.BreakEntry.LimitZmin=nil @@ -2598,8 +2607,8 @@ function AIRBOSS:_InitStennis() self.Abeam.name="Abeam Position" self.Abeam.Xmin= nil self.Abeam.Xmax= nil - self.Abeam.Zmin=-UTILS.NMToMeters(3) -- Not more than 3 NM port. - self.Abeam.Zmax= 0 -- Must be port! + self.Abeam.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.Abeam.Zmax= 100 -- Must be port! self.Abeam.LimitXmin=-200 -- Check and next step 200 meters behind the ship. self.Abeam.LimitXmax= nil self.Abeam.LimitZmin= nil @@ -2675,7 +2684,7 @@ function AIRBOSS:_GetAircraftAoA(playerData) aoa.Fast=6.9 aoa.FAST=6.3 elseif skyhawk then - -- A-4E-C parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 + -- A-4E-C Skyhawk parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 aoa.SLOW=19.0 aoa.Slow=18.5 aoa.OnSpeedMax=18.0 @@ -2684,7 +2693,7 @@ function AIRBOSS:_GetAircraftAoA(playerData) aoa.Fast=16.5 aoa.FAST=16.0 elseif harrier then - -- TODO: AV-8B parameters! On speed AoA? + -- AV-8B Harrier parameters. This might need further tuning. aoa.SLOW=14.0 aoa.Slow=13.0 aoa.OnSpeedMax=12.0 @@ -2930,16 +2939,19 @@ end --- Scan carrier zone for (new) units. -- @param #AIRBOSS self function AIRBOSS:_ScanCarrierZone() - self:T(self.lid.."Scanning Carrier Zone") - + -- Carrier position. local coord=self:GetCoordinate() - -- Scan radius. - local Rout=UTILS.NMToMeters(50) + -- Scan radius = radius of the CCA. + --local Rout=UTILS.NMToMeters(50) + local RCCZ=self.zoneCCA:GetRadius() + + -- Debug info. + self:T(self.lid..string.format("Scanning Carrier Controlled Area. Radius=%.1f NM.", UTILS.MetersToNM(RCCZ))) -- Scan units in carrier zone. - local _,_,_,unitscan=coord:ScanObjects(Rout, true, false, false) + local _,_,_,unitscan=coord:ScanObjects(RCCZ, true, false, false) -- Make a table with all groups currently in the CCA zone. @@ -3030,8 +3042,10 @@ function AIRBOSS:_ScanCarrierZone() for _,_flight in pairs(self.flights) do local flight=_flight --#AIRBOSS.FlightGroup if insideCCA[flight.groupname]==nil then - -- TODO: do not remove flights in marshal pattern. At least for case 3. if zone is set small, they might get out! - table.insert(remove, flight.group) + -- Do not remove flights in marshal pattern. At least for case 2 & 3. If zone is set small, they might be outside in the holding pattern. + if not (flight.case>1 and self:_InQueue(self.Qmarshal, flight.group)) then + table.insert(remove, flight.group) + end end end @@ -3305,7 +3319,7 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) p1=Carrier -- Second point 1.5 NM ahead. - p2=Carrier:Translate( UTILS.NMToMeters(1.5), hdg) + p2=Carrier:Translate(UTILS.NMToMeters(1.5), hdg) else @@ -3320,9 +3334,11 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) -- For CCW pattern: p1 further astern than p2. + -- Length of the race track pattern. + local l=UTILS.NMToMeters(7) + -- First point of race track pattern. - --TODO: check if 7 NM is okay. - p1=Carrier:Translate(Dist+UTILS.NMToMeters(7), radial) + p1=Carrier:Translate(Dist+l, radial) -- Second point. p2=Carrier:Translate(Dist, radial) @@ -3362,6 +3378,13 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) -- TODO: Get charlie time estimate. local text=string.format("Case %d, BRC is %03d, hold at %d. Expected Charlie Time XX.\n", flight.case, brc, alt) text=text..string.format("Altimeter %.2f. Report see me.", P) + + -- Hint about TACAN bearing. + if self.TACANon and (not flight.ai) and flight.difficulty==AIRBOSS.Difficulty.EASY then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(flight.case, true, true, true) + text=text..string.format("\nSelect TACAN %d°, channel %d%s (%s)", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + end -- Message to all players. self:MessageToAll(text, "MARSHAL", flight.onboard) @@ -3411,6 +3434,9 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- Stack of flight. local stack=flight.flag:Get() + + -- Memorize time when stack collapsed. Should better depend on case but for now we assume there are no two different stacks Case I or II/III. + self.Tcollapse=timer.getTime() -- Decrease flag values of all flight groups in marshal stack. for _,_flight in pairs(self.Qmarshal) do @@ -4097,7 +4123,7 @@ function AIRBOSS:_CheckPatternUpdate() end -- Inform player about new final bearing. - if Dchange then + if Hchange then -- 99, new final bearing XXX local FB=self:GetFinalBearing(true) local text=string.format("new final bearing %d.", FB) @@ -4134,7 +4160,7 @@ function AIRBOSS:_CheckPlayerStatus() -- Display aircraft attitude and other parameters as message text. if playerData.attitudemonitor then - self:_DetailedPlayerStatus(playerData) + self:_AttitudeMonitor(playerData) end -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). @@ -4149,7 +4175,7 @@ function AIRBOSS:_CheckPlayerStatus() playerData.step==AIRBOSS.PatternStep.ABEAM or playerData.step==AIRBOSS.PatternStep.GROOVE_XX or playerData.step==AIRBOSS.PatternStep.GROOVE_IM then - self:_CheckPlayerPatternDistance(playerData) + --self:_CheckPlayerPatternDistance(playerData) end if playerData.step==AIRBOSS.PatternStep.UNDEFINED then @@ -4270,12 +4296,12 @@ function AIRBOSS:_CheckPlayerStatus() end else - self:E(self.lid.."WARNING: Player left the CCA!") + self:T(self.lid.."WARNING: Player left the CCA!") end else -- Unit not alive. - self:E(self.lid.."WARNING: Player unit is not alive!") + self:T(self.lid.."WARNING: Player unit is not alive!") end end end @@ -4330,7 +4356,6 @@ function AIRBOSS:OnEventBirth(EventData) -- Welcome player message. self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), "AIRBOSS", "", 5) - end end @@ -4372,8 +4397,6 @@ function AIRBOSS:OnEventLand(EventData) local _group=_unit:GetGroup() local _callsign=_unit:GetCallsign() - -- TODO: also check distance to airbase since landing "in the water" also trigger a landing event! - -- Debug output. local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename) self:T(self.lid..text) @@ -4385,7 +4408,7 @@ function AIRBOSS:OnEventLand(EventData) -- Check that player landed on the carrier. if _unit:IsInZone(zoneCarrier) then - -- Check if player already landed. We dont need a second time. + -- Check if player already landed. We dont need a second time. if playerData.landed then self:E(self.lid..string.format("Player %s just landed a second time.", _playername)) @@ -4400,8 +4423,7 @@ function AIRBOSS:OnEventLand(EventData) -- Landing distance wrt to stern position. local dist=coord:Get2DDistance(stern) - - + -- Debug mark of player landing coord. if self.Debug and false then -- Debug mark of player landing coord. @@ -4410,7 +4432,7 @@ function AIRBOSS:OnEventLand(EventData) end -- Get wire. We additionally shift the landing coord back because landing event for players is unfortunately delayed. - local wire=self:_GetWire(coord, 100) + local wire=self:_GetWire(coord, 75) -- No wire ==> Bolter, Bolter radio call. -- TODO: might need a better place for this. or check @@ -4420,11 +4442,15 @@ function AIRBOSS:OnEventLand(EventData) -- Get time in the groove. local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData - playerData.Tgroove=timer.getTime()-gdataX0.TGroove + if gdataX0 then + playerData.Tgroove=timer.getTime()-gdataX0.TGroove + else + playerData.Tgroove=999 + end -- Debug text. local text=string.format("Player %s AC type %s landed at dist=%.1f m. Trapped wire=%d.", playerData.name, playerData.actype, dist, wire) - text=text..string.format("X=%.1f m, Z=%.1f m, rho=%.1f m, phi=%.1f deg.", X, Z, rho, phi) + text=text..string.format(" X=%.1f m, Z=%.1f m, rho=%.1f m.", X, Z, rho) self:T(self.lid..text) -- We did land. @@ -4439,7 +4465,8 @@ function AIRBOSS:OnEventLand(EventData) end else - -- Player did not land in carrier box zone. Maybe in the water near the carrier. + -- TODO: Handle case where player did not land on the carrier. + self:E(self.lid..string.format("Player %s did not land in carrier box zone. Maybe in the water near the carrier?", playerData.name)) end else @@ -4459,8 +4486,8 @@ function AIRBOSS:OnEventLand(EventData) local _type=EventData.IniUnit:GetTypeName() -- Debug text. - local text=string.format("AI %s of type %s landed at dist=%.1f m. Trapped wire=%d.", EventData.IniUnitName, _type, dist, wire) - self:T2(self.lid..text) + local text=string.format("AI unit %s of type %s landed at dist=%.1f m. Trapped wire=%d.", _unitName, _type, dist, wire) + self:T(self.lid..text) -- AI always lands ==> remove unit from flight group and queues. self:_RemoveUnitFromFlight(EventData.IniUnit) @@ -4560,6 +4587,18 @@ function AIRBOSS:_Holding(playerData) -- ERROR end + -- Check if stack just collapsed and give the player one minute to change the alitude. + local justcollapsed=false + if self.Tcollapse then + -- Time since last stack change. + local dT=timer.getTime()-self.Tcollapse + + -- Check if less then 60 seconds. + if dT<=60 then + justcollapsed=true + end + end + -- Check if altitude is acceptable. local goodalt=math.abs(altdiff)altgood then + -- Altitude check if stack not just collapsed. + if not justcollapsed then - -- Issue warning for being too high. - if not playerData.warning then - text=text..string.format("You left your assigned altitude. Descent to angels %d.", angels) - playerData.warning=true + if altdiff>altgood then + + -- Issue warning for being too high. + if not playerData.warning then + text=text..string.format("You left your assigned altitude. Descent to angels %d.", angels) + playerData.warning=true + end + + elseif altdiff<-altgood then + + -- Issue warning for being too low. + if not playerData.warning then + text=text..string.format("You left your assigned altitude. Climb to angels %d.", angels) + playerData.warning=true + end + end - elseif altdiff<-altgood then - - -- Issue warning for being too low. - if not playerData.warning then - text=text..string.format("You left your assigned altitude. Climb to angels %d.", angels) - playerData.warning=true - end - - else - - -- Back to assigned altitude. - if playerData.warning then - text=text..string.format("Altitude is looking good again.") - playerData.warning=nil - end + end + -- Back to assigned altitude. + if playerData.warning and math.abs(altdiff)<=altgood then + text=text..string.format("Altitude is looking good again.") + playerData.warning=nil end elseif playerData.holding==false then @@ -4680,15 +4721,10 @@ function AIRBOSS:_Commencing(playerData) -- Initialize player data for new approach. self:_InitPlayer(playerData) - - -- Commence - local text=string.format("Commencing. (Case %d)", playerData.case) - -- Message to all players in Marshal stack. - --self:MessageToMarshal(text, playerData.onboard, "", 5) - - -- Message to player only. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + -- Commencing message to player only. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + local text=string.format("Commencing. (Case %d)", playerData.case) self:MessageToPlayer(playerData, text, playerData.onboard, "", 5) end @@ -4712,7 +4748,13 @@ end function AIRBOSS:_Initial(playerData) -- Check if player is in initial zone and entering the CASE I pattern. - if playerData.unit:IsInZone(self.zoneInitial) then + local inzone=playerData.unit:IsInZone(self.zoneInitial) + + -- Relative heading to carrier direction. + local relheading=self:_GetRelativeHeading(playerData.unit, false) + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 then -- Send message for normal and easy difficulty. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then @@ -4748,13 +4790,13 @@ function AIRBOSS:_CheckCorridor(playerData) local invalid=playerData.unit:IsNotInZone(validzone) -- Issue warning. - if invalid and not playerData.warning then + if invalid and (not playerData.warning) then self:MessageToPlayer(playerData, "You left the valid approach corridor!", "MARSHAL") playerData.warning=true end -- Back in zone. - if not invalid and playerData.warning then + if (not invalid) and playerData.warning then self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARSHAL") playerData.warning=false end @@ -4771,7 +4813,7 @@ function AIRBOSS:_Platform(playerData) -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZonePlatform(playerData.case)) - + -- Check if we are in zone. if inzone then @@ -4849,7 +4891,11 @@ function AIRBOSS:_ArcInTurn(playerData) if playerData.difficulty==AIRBOSS.Difficulty.EASY then -- Get inverse magnetic radial without offset ==> FB for Case II or BRC for Case III. local radial=self:GetRadial(playerData.case, true, false, true) - hint=hint..string.format("\nTurn right and select TACAN %d.", radial) + local turn="right" + if self.holdingoffset<0 then + turn="left" + end + hint=hint..string.format("\nTurn %s and select TACAN %d°.", turn, radial) end -- Message to player. @@ -4972,9 +5018,12 @@ function AIRBOSS:_Bullseye(playerData) -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) - -- Check that we reached the position. - if inzone then - + -- Relative heading to carrier direction of the runway. + local relheading=self:_GetRelativeHeading(playerData.unit, true) + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 then + -- Debug message. MESSAGE:New("Bullseye step reached", 5, "DEBUG"):ToAllIf(self.Debug) @@ -5089,7 +5138,7 @@ function AIRBOSS:_Break(playerData, part) -- Hint dirty up. if playerData.difficult==AIRBOSS.Difficulty.EASY and part==AIRBOSS.PatternStep.LATEBREAK then - hint=hint.."Dirty up! Gear down, flaps down. Check hook down." + hint=hint.."\nDirty up! Gear down, flaps down. Check hook down." end self:MessageToPlayer(playerData, hint, "MARSHAL", "") @@ -5397,14 +5446,14 @@ function AIRBOSS:_Groove(playerData) local AoA=playerData.unit:GetAoA() -- For debugging. - --MESSAGE:New(string.format("LineUp=%.1f GlideSlope=%.1f AoA=%.1f", lineupError, glideslopeError, AoA), 3, nil, true):ToAll() + MESSAGE:New(string.format("%s: LineUp=%.1f GlideSlope=%.1f AoA=%.1f", playerData.step, lineupError, glideslopeError, AoA), 3, nil, true):ToAllIf(self.Debug) -- Ranges in the groove. - local RXX=UTILS.NMToMeters(0.750)+math.abs(self.carrierparam.sterndist) -- Start of groove. 0.75 = 1389 m - local RRB=UTILS.NMToMeters(0.500)+math.abs(self.carrierparam.sterndist) -- Roger Ball! call. 0.5 = 926 m - local RIM=UTILS.NMToMeters(0.375)+math.abs(self.carrierparam.sterndist) -- In the Middle 0.75/2. 0.375 = 695 m - local RIC=UTILS.NMToMeters(0.100)+math.abs(self.carrierparam.sterndist) -- In Close. 0.1 = 185 m - local RAR=UTILS.NMToMeters(0.000)+math.abs(self.carrierparam.sterndist) -- At the Ramp. + local RXX=UTILS.NMToMeters(0.750) -- Start of groove. 0.75 = 1389 m + local RRB=UTILS.NMToMeters(0.500) -- Roger Ball! call. 0.5 = 926 m + local RIM=UTILS.NMToMeters(0.375) -- In the Middle 0.75/2. 0.375 = 695 m + local RIC=UTILS.NMToMeters(0.100) -- In Close. 0.1 = 185 m + local RAR=UTILS.NMToMeters(0.000) -- At the Ramp. -- Data local groovedata={} --#AIRBOSS.GrooveData @@ -5477,6 +5526,9 @@ function AIRBOSS:_Groove(playerData) -- Check if player should wave off. local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) + --local text=string.format("FF D=%.1f GLE=%.1f LUE=%.1f waveoff=%s", rho, glideslopeError, lineupError, tostring(waveoff)) + --env.info(text) + -- Let's see.. if waveoff then @@ -5515,7 +5567,7 @@ function AIRBOSS:_Groove(playerData) local deltaT=timer.getTime()-playerData.Tlso -- Check if we are beween 3/4 NM and end of ship. Only one call every 3 seconds. - if X<0 and rho>=RAR and rho=3 and playerData.waveoff==false then + if X=RAR and rho=3 and playerData.waveoff==false then -- LSO call if necessary. self:_LSOadvice(playerData, glideslopeError, lineupError) @@ -5545,7 +5597,13 @@ function AIRBOSS:_Groove(playerData) else - -- What? Player was not waved off but flew past the carrier without landing. Why did waveoff not kick in? + -- This should not happen. + self:E("What? Player was not waved off but flew past the carrier without landing. Why did waveoff not kick in?") + + -- TODO: This is more like a pilot wave off then. + self:_AddToDebrief(playerData, "Pilot wave-off.") + + playerData.waveoff=true end @@ -5577,7 +5635,7 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) -- Too high or too low? if math.abs(glideslopeError)>1 then local text=string.format("Wave off due to glide slope error |%.1f| > 1 degree!", glideslopeError) - self:I(self.lid..string.format("%s: %s", playerData.name, text)) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) self:_AddToDebrief(playerData, text) waveoff=true end @@ -5585,7 +5643,7 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) -- Too far from centerline? if math.abs(lineupError)>3 then local text=string.format("Wave off due to line up error |%.1f| > 3 degrees!", lineupError) - self:I(self.lid..string.format("%s: %s", playerData.name, text)) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) self:_AddToDebrief(playerData, text) waveoff=true end @@ -5597,12 +5655,12 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) -- Check too slow or too fast. if AoAaoaac.Slow then local text=string.format("Wave off due to AoA %.1f > %.1f!", AoA, aoaac.Slow) - self:I(self.lid..string.format("%s: %s", playerData.name, text)) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) self:_AddToDebrief(playerData, text) waveoff=true end @@ -5705,7 +5763,7 @@ function AIRBOSS:_GetWire(Lcoord, dc) end -- Debug output. - self:I(string.format("GetWire: L=%.1f, L-dc=%.1f ==> wire=%d (dc=%.1f)", Ldist, Ldist-dc, wire, dc)) + self:T(string.format("GetWire: L=%.1f, L-dc=%.1f ==> wire=%d (dc=%.1f)", Ldist, Ldist-dc, wire, dc)) return wire end @@ -5739,7 +5797,7 @@ function AIRBOSS:_Trapped(playerData) -- Debug. local text=string.format("Player %s _Trapped: v=%.1f km/h, s=%.1f m ==> wire=%d (dcorr=%d)", playerData.name, v, s, wire, dcorr) - self:E(self.lid..text) + self:T(self.lid..text) -- Call this function again until v < threshold. Player comes to a standstill ==> Get wire! if v>5 then @@ -5921,7 +5979,7 @@ function AIRBOSS:_GetZonePlatform(case) local alpha=math.rad(self.holdingoffset) -- Distance = 19 NM - local distance=UTILS.NMToMeters(19)/math.cos(alpha) + local distance=UTILS.NMToMeters(19) --/math.cos(alpha) -- Get coordinate. local coord=self:GetCoordinate():Translate(distance, radial) @@ -5945,17 +6003,22 @@ function AIRBOSS:_GetZoneCorridor(case) -- Angle between radial and offset in rad. local alpha=math.rad(self.holdingoffset) + + -- Distance shift ahead of carrier to allow for some space to bolter + local dx=2 -- Width of the box in NM. local w=2 local w2=w/2 - - -- Length of the box in NM. - local l=10/math.cos(alpha) -- Distance from carrier to arc out zone. local d=12 + -- Length of the box in NM. + local x=(d+w/2)/math.cos(alpha) + local l=28-x + --local l=15 --/math.cos(alpha) + -- Some math... local y1=d-w2 local x1=y1*math.tan(alpha) @@ -5982,24 +6045,24 @@ function AIRBOSS:_GetZoneCorridor(case) self:T3(string.format("FF Q = %.1f NM", Q)) local c={} - c[1]=self:GetCoordinate() --Carrier coordinate + c[1]=self:GetCoordinate():Translate(-UTILS.NMToMeters(dx), radial) --Carrier coordinate translated 2 NM in direction of travel to allow for bolter space. if math.abs(self.holdingoffset)>1 then -- Complicated case with an angle. - c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier CORRECT! - c[3]=c[2]:Translate( UTILS.NMToMeters(d+w2), radial) -- 13 "south" @ 1 right - c[4]=c[3]:Translate( UTILS.NMToMeters(Q), radial+90) -- - c[5]=c[4]:Translate( UTILS.NMToMeters(l), offset) - c[6]=c[5]:Translate( UTILS.NMToMeters(w), offset+90) -- Back wall (angled) - c[9]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) -- 1 left of carrier CORRECT! - c[8]=c[9]:Translate( UTILS.NMToMeters(d-w2), radial) -- 1 left and 11 behind of carrier CORRECT! - c[7]=c[8]:Translate( UTILS.NMToMeters(P), radial+90) + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier. + c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2), radial) -- 13 "south" @ 1 right + c[4]=c[3]:Translate( UTILS.NMToMeters(Q), radial+90) -- + c[5]=c[4]:Translate( UTILS.NMToMeters(l), offset) + c[6]=c[5]:Translate( UTILS.NMToMeters(w), offset+90) -- Back wall (angled) + c[9]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) -- 1 left of carrier. + c[8]=c[9]:Translate( UTILS.NMToMeters(d+dx-w2), radial) -- 1 left and 11 behind of carrier. + c[7]=c[8]:Translate( UTILS.NMToMeters(P), radial+90) else -- Easy case of a long box. - c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) - c[3]=c[2]:Translate( UTILS.NMToMeters(d+w2+l), radial) - c[4]=c[3]:Translate( UTILS.NMToMeters(w), radial+90) - c[5]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) + c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2+l), radial) -- 12+1+10 = 23 NM behind the carrier. Stack 1 starts at 21 and is 7 NM. + c[4]=c[3]:Translate( UTILS.NMToMeters(w), radial+90) + c[5]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) end @@ -6131,12 +6194,12 @@ function AIRBOSS:_GetZoneHolding(case, stack) -- Create an array of a square! local p={} - p[1]=c1:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c1 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. - p[2]=c2:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c2 is 10 NM further behind. Also translated 1 NM starboard. - p[3]=c2:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p3 6 NM port of carrier. - p[4]=c1:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p4 6 NM port of carrier. + p[1]=c2:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c2 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. + p[2]=c1:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c1 is 7 NM further behind. Also translated 1 NM starboard. + p[3]=c1:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p3 6 NM port of carrier. + p[4]=c2:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p4 6 NM port of carrier. - -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. + -- Square zone length=7NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. -- So stay 0-5 NM (+1 NM error margin) port of carrier. zoneHolding=ZONE_POLYGON_BASE:New("CASE II/III Holding Zone", p) end @@ -6151,7 +6214,7 @@ end --- Provide info about player status on the fly. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_DetailedPlayerStatus(playerData) +function AIRBOSS:_AttitudeMonitor(playerData) -- Player unit. local unit=playerData.unit @@ -6183,7 +6246,7 @@ function AIRBOSS:_DetailedPlayerStatus(playerData) text=text..string.format("Pitch=%.1f° | Roll=%.1f° | Yaw=%.1f°\n", pitch, roll, yaw) text=text..string.format("Climb Angle=%.1f° | Rate=%d ft/min\n", unit:GetClimbAngle(), velo.y*196.85) text=text..string.format("R=%.1f NM | X=%d Z=%d m\n", UTILS.MetersToNM(rho), dx, dz) - text=text..string.format("Phi=%.1f° | Rel=%.1f°", phi, relhead) + text=text..string.format("Gamma=%.1f°", relhead) -- If in the groove, provide line up and glide slope error. if playerData.step==AIRBOSS.PatternStep.GROOVE_XX or playerData.step==AIRBOSS.PatternStep.GROOVE_RB or @@ -6200,7 +6263,7 @@ function AIRBOSS:_DetailedPlayerStatus(playerData) -- Wind (for debugging). --text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) - MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) + MESSAGE:New(text, 3, nil , true):ToClient(playerData.client) end --- Get glide slope of aircraft unit. @@ -6218,7 +6281,7 @@ function AIRBOSS:_Glideslope(unit, optangle) -- Ideally we want to land between 2nd and 3rd wire. if self.carrierparam.wire3 then - local d23=self.carrierparam.wire2+0.5*(self.carrierparam.wire3-self.carrierparam.wire2) + local d23=self.carrierparam.wire2 --+0.5*(self.carrierparam.wire3-self.carrierparam.wire2) stern=stern:Translate(d23, self:GetFinalBearing(false), true) end @@ -6781,7 +6844,7 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) -- No flight data ==> return empty string. if fdata==nil then - self:E(self.lid.."Flight data is nil.") + self:T(self.lid.."Flight data is nil.") return "", 0 end @@ -6912,16 +6975,16 @@ function AIRBOSS:_CheckAbort(X, Z, pos) local abort=false if pos.Xmin and Xpos.Xmax then - self:E(string.format("Xmax: X=%d > %d=Xmax", X, pos.Xmax)) + self:T(string.format("Xmax: X=%d > %d=Xmax", X, pos.Xmax)) abort=true elseif pos.Zmin and Zpos.Zmax then - self:E(string.format("Zmax: Z=%d > %d=Zmax", Z, pos.Zmax)) + self:T(string.format("Zmax: Z=%d > %d=Zmax", Z, pos.Zmax)) abort=true end @@ -7005,34 +7068,40 @@ function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) -- Debug. local dtext=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..dtext) - --MESSAGE:New(text, 60):ToAllIf(self.Debug) + + -- Message to player. + self:MessageToPlayer(playerData, text, "LSO", nil, 20) if patternwo then -- Pattern wave off! playerData.patternwo=true - -- Tell player to depart. - text=text.." Depart and re-enter!" - -- Add to debrief. self:_AddToDebrief(playerData, string.format("Pattern wave off: %s", text)) + + -- Depart and re-enter radio message. + -- TODO: Radio should depend on player step. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.DEPARTANDREENTER, false, 3) -- Next step debrief. playerData.step=AIRBOSS.PatternStep.DEBRIEF playerData.warning=nil end - - -- Message to player. - self:MessageToPlayer(playerData, text, "LSO", nil, 20) + end ---- Evaluate player's altitude at checkpoint. +--- Get error margin depending on player skill. +-- +-- * Flight students: 10% and 20% +-- * Naval Aviators: 5% and 10% +-- * TOPGUN Graduates: 2.5% and 5% +-- -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. --- @return #number Low score. --- @return #number Bad score. +-- @return #number Error margin for still being okay. +-- @return #number Error margin for really sucking. function AIRBOSS:_GetGoodBadScore(playerData) local lowscore @@ -7052,7 +7121,6 @@ function AIRBOSS:_GetGoodBadScore(playerData) end - --- Evaluate player's altitude at checkpoint. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -7103,10 +7171,12 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) -- Extend or decrease depending on skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about the optimal altitude. hint=hint..string.format(" Optimal altitude is %d ft.", UTILS.MetersToFeet(altopt)) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then - --hint=hint.."\n" + -- We keep it short normally. elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. hint="" end @@ -7152,10 +7222,12 @@ function AIRBOSS:_DistanceCheck(playerData, optdist) -- Extend or decrease depending on skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about optimal value. hint=hint..string.format(" Optimal distance is %.1f NM.", UTILS.MetersToNM(optdist)) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then - --hint=hint.."\n" + -- We keep it short normally. elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. hint="" end @@ -7176,7 +7248,7 @@ function AIRBOSS:_AoACheck(playerData, optaoa) if optaoa==nil then return nil, nil end - + -- Get relative score. local lowscore, badscore = self:_GetGoodBadScore(playerData) @@ -7184,27 +7256,35 @@ function AIRBOSS:_AoACheck(playerData, optaoa) local aoa=playerData.unit:GetAoA() -- Altitude error +-X% - local _error=(aoa-optaoa)/optaoa*100 + local _error=(aoa-optaoa)/optaoa*100 + + -- Get aircraft AoA parameters. + local aircraftaoa=self:_GetAircraftAoA(playerData) - local hint - if _error>badscore then --Slow - hint="You're slow. " - elseif _error>lowscore then --Slightly slow - hint="You're slightly slow. " - elseif _error<-badscore then --Fast - hint="You're fast. " - elseif _error<-lowscore then --Slightly fast - hint="You're slightly fast. " - else --On speed - hint="You're on speed. " + -- Rate aoa. + local hint="" + if aoa>=aircraftaoa.SLOW then + hint="Your're slow!" + elseif aoa>=aircraftaoa.Slow then + hint="Your're slow." + elseif aoa>=aircraftaoa.OnSpeedMax then + hint="Your're a little slow." + elseif aoa>=aircraftaoa.OnSpeedMin then + hint="You're on speed." + elseif aoa>=aircraftaoa.Fast then + hint="You're a little fast." + else + hint="You're fast!" end -- Extend or decrease depending on skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about optimal value. hint=hint..string.format(" Optimal AoA is %.1f.", optaoa) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then - --hint=hint.."\n" + -- We keep is short normally. elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. hint="" end @@ -7252,13 +7332,14 @@ function AIRBOSS:_SpeedCheck(playerData, speedopt) if playerData.difficulty==AIRBOSS.Difficulty.EASY then hint=hint..string.format(" Optimal speed is %d knots.", UTILS.MpsToKnots(speedopt)) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then - --hint=hint.."\n" + -- We keep is short normally. elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for pros. hint="" end -- Debrief text. - local debrief=string.format("Speed %d knots = %d%% deviation from %d knots optimum.", UTILS.MpsToKnots(speed), _error, UTILS.MpsToKnots(speedopt)) + local debrief=string.format("Speed %d knots = %d%% deviation from %d knots.", UTILS.MpsToKnots(speed), _error, UTILS.MpsToKnots(speedopt)) return hint, debrief end @@ -7269,7 +7350,6 @@ end -- @param #string hint Debrief text of this step. -- @param #string step (Optional) Current step in the pattern. Default from playerData. function AIRBOSS:_AddToDebrief(playerData, hint, step) - playerData.debrief={} step=step or playerData.step table.insert(playerData.debrief, {step=step, hint=hint}) end @@ -7301,7 +7381,7 @@ function AIRBOSS:_Debrief(playerData) if not playerData.patternwo then -- Wire trapped. Not if pattern WI. - if playerData.wire then + if playerData.wire and playerData.wire<=4 then text=text..string.format(" %d-wire", playerData.wire) end @@ -7360,7 +7440,7 @@ function AIRBOSS:_Debrief(playerData) elseif playerData.case==3 then -- Next step? Bullseye for now. - -- TODO: Could be DIRTY UP or PLATFORM or even back to MARSHAL STACK? + -- TODO: Could be DIRTY UP or PLATFORM or even back to MARSHAL STACK? playerData.step=AIRBOSS.PatternStep.BULLSEYE -- Get heading and distance to bullseye zone ~3 NM astern. @@ -7382,7 +7462,7 @@ function AIRBOSS:_Debrief(playerData) self:T2(self.lid..string.format("Player unit not alive!")) end - + elseif playerData.waveoff then -------------- @@ -7453,9 +7533,6 @@ function AIRBOSS:_Debrief(playerData) -- Remove player unit from flight and all queues. self:_RemoveUnitFromFlight(playerData.unit) - -- Message to player. - --self:MessageToPlayer(playerData, string.format("Welcome aboard, %s!", playerData.name), "LSO", "", 10) - -- Welcome aboard! self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) @@ -7504,12 +7581,12 @@ function AIRBOSS:_StepHint(playerData, step) -- Altitude. if alt then - hint=hint..string.format("\nAltitude=%d ft", UTILS.MetersToFeet(alt)) + hint=hint..string.format("\nAltitude %d ft", UTILS.MetersToFeet(alt)) end -- AoA. if aoa then - hint=hint..string.format("\nAoA=%.1f", aoa) + hint=hint..string.format("\nAoA %.1f", aoa) end -- Speed. @@ -7835,7 +7912,6 @@ end -- @param #table radioqueue The radio queue. -- @param #string name Name of the queue. function AIRBOSS:_CheckRadioQueue(radioqueue, name) - --env.info(string.format("FF: check radio queue %s: n=%d", name, #radioqueue)) -- Check if queue is empty. if #radioqueue==0 then @@ -7976,6 +8052,14 @@ function AIRBOSS:RadioTransmit(radio, call, loud, delay) -- Broadcast message. radio:Broadcast(true) + -- Workaround for the community A-4E-C as long as their radios are not functioning properly. + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + if playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + USERSOUND:New(filename):ToGroup(playerData.group) + end + end + -- Message "Subtitle" to all players. self:MessageToAll(subtitle, radio:GetAlias(), "", call.duration) @@ -8022,26 +8106,35 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration SCHEDULER:New(self, self.MessageToPlayer, {playerData, message, sender, receiver, duration, clear, 0, soundoff}, delay) else - -- Send onboard number so that player is alerted about the text message. - if (receiver==playerData.onboard or receiver=="99") and (not soundoff) then - if sender then + if sender and not soundoff then + + if receiver=="99" then + + -- Radio message from LSO or MARSHAL to all. if sender=="LSO" then - self:_Number2Sound(self.LSORadio, receiver, delay) + self:_Number2Radio(self.LSORadio, receiver, delay) elseif sender=="MARSHAL" then - self:_Number2Sound(self.MarshalRadio, receiver, delay) + self:_Number2Radio(self.MarshalRadio, receiver, delay) end - end - end - + + elseif receiver==playerData.onboard then + + -- Sound only to player group. + if sender=="LSO" or sender=="MARSHAL" then + self:_Number2Sound(playerData, sender, receiver, delay) + end + + end + end + -- Text message to player client. if playerData.client then MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) - end - + end + end end - end --- Send text message to all players in the CCA. @@ -8137,6 +8230,66 @@ function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, de end end +--- Convert a number (as string) into an outsound and play it to a player group. E.g. for board number or headings. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string sender Who is sending the call, either "LSO" or "MARSHAL". +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +function AIRBOSS:_Number2Sound(playerData, sender, number, delay) + + --- Split string into characters. + local function _split(str) + local chars={} + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + return chars + end + + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(self, AIRBOSS._Number2Sound, {playerData, sender, number}, delay) + else + + -- Split string into characters. + local numbers=_split(number) + + local Sender + if sender=="LSO" then + Sender="LSOCall" + elseif sender=="MARSHAL" then + Sender="MarshalCall" + else + self:E(self.lid..string.format("ERROR: Unknown radio sender %s!", tostring(sender))) + return + end + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Convert to N0, N1, ... + local N=string.format("N%s", n) + + -- Radio call. + local call=AIRBOSS[Sender][N] --#AIRBOSS.RadioCall + + -- Create file name. + local filename=string.format("%s.%s", call.file, call.suffix) + + -- Play sound. + USERSOUND:New(filename):ToGroup(playerData.group, wait) + + -- Wait until this call is over before playing the next. + wait=wait+call.duration + end + + end +end --- Convert a number (as string) into a radio message. -- E.g. for board number or headings. @@ -8144,7 +8297,7 @@ end -- @param Core.Radio#RADIO radio Radio used for transmission. -- @param #string number Number string, e.g. "032" or "183". -- @param #number delay Delay before transmission in seconds. -function AIRBOSS:_Number2Sound(radio, number, delay) +function AIRBOSS:_Number2Radio(radio, number, delay) --- Split string into characters. local function _split(str) @@ -8405,12 +8558,10 @@ function AIRBOSS:_RequestMarshal(_unitName) -- Flight group is already in pattern queue. local text=string.format("you are not airborne. Marshal request denied!") - self:MessageToPlayer(playerData, text, "MARSHAL") + self:MessageToPlayer(playerData, text, "MARSHAL") else - - -- TODO: check if recovery window is open. - + -- Add flight to marshal stack. self:_MarshalPlayer(playerData) @@ -8443,7 +8594,7 @@ function AIRBOSS:_RequestCommence(_unitName) if playerData then -- Check if unit is in CCA. - local text + local text="" if _unit:IsInZone(self.zoneCCA) then if self:_InQueue(self.Qpattern, playerData.group) then @@ -8477,12 +8628,17 @@ function AIRBOSS:_RequestCommence(_unitName) text=string.format("Negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern) else + + -- TODO: check if recovery window is open. + if not self:IsRecovering() then + text="Recovery window NOT open yet! However, you are cleared anyway.\n" + end -- Positive response. if playerData.case==1 then - text="Proceed to initial." + text=text.."Proceed to initial." else - text="Descent at 4k ft/min to platform at 5000 ft." + text=text.."Descent at 4k ft/min to platform at 5000 ft." end -- Set player step. @@ -8505,7 +8661,7 @@ function AIRBOSS:_RequestCommence(_unitName) self:T(self.lid..text) -- Send message. - self:MessageToPlayer(playerData, text, "MARSHAL") + self:MessageToPlayer(playerData, text, "MARSHAL") end end end @@ -8686,8 +8842,6 @@ function AIRBOSS:_DisplayScoreBoard(_unitName) text=text..string.format("\n[%d] %.1f %s", i,_points,_playerName) i=i+1 end - - --env.info("FF:\n"..text) -- Send message. if playerData.client then @@ -8720,6 +8874,18 @@ function AIRBOSS:_DisplayPlayerGrades(_unitName) local grade=_grade --#AIRBOSS.LSOgrade text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, grade.points, grade.details) + + -- Wire trapped if any. + if grade.wire and grade.wire<=4 then + text=text..string.format(" %d-wire", grade.wire) + end + + -- Time in the groove if any. + if grade.Tgroove and grade.Tgroove<=60 then + text=text..string.format(" Tgroove=%.1f s", grade.Tgroove) + end + + -- Add up points. p=p+grade.points end @@ -8732,8 +8898,6 @@ function AIRBOSS:_DisplayPlayerGrades(_unitName) text=text..string.format("\nNo data available.") end - --env.info("FF:\n"..text) - -- Send message. if playerData.client then MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) @@ -8789,7 +8953,7 @@ end -- @param #string playername Player name. -- @param #AIRBOSS.Difficulty difficulty Difficulty level. function AIRBOSS:_SetDifficulty(playername, difficulty) - self:E({difficulty=difficulty, playername=playername}) + self:T2({difficulty=difficulty, playername=playername}) local playerData=self.players[playername] --#AIRBOSS.PlayerData @@ -8810,7 +8974,7 @@ end -- @param #AIRBOSS self -- @param #string playername Player name. function AIRBOSS:_AttitudeMonitor(playername) - self:E({playername=playername}) + self:F2({playername=playername}) local playerData=self.players[playername] --#AIRBOSS.PlayerData @@ -8824,7 +8988,7 @@ end -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. function AIRBOSS:_DisplayCarrierInfo(_unitname) - self:E(_unitname) + self:F2(_unitname) -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) @@ -8891,7 +9055,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) text=text..string.format("BRC %03d°\n", self:GetBRC()) text=text..string.format("FB %03d°\n", self:GetFinalBearing(true)) text=text..string.format("Speed %d kts\n", carrierspeed) - text=text..string.format("Marshal radio %.3f MHz\n", self.MarshalFreq) --TODO: add modulation + text=text..string.format("Marshal radio %.3f MHz\n", self.MarshalFreq) text=text..string.format("LSO radio %.3f MHz\n", self.LSOFreq) text=text..string.format("TACAN Channel %s\n", tacan) text=text..string.format("ICLS Channel %s\n", icls) @@ -8916,11 +9080,10 @@ end -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. function AIRBOSS:_DisplayCarrierWeather(_unitname) - self:E(_unitname) + self:F2(_unitname) -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) - self:E({playername=playername}) -- Check if we have a player. if unit and playername then @@ -9085,7 +9248,7 @@ function AIRBOSS:_MarkMarshalZone(_unitName, flare) end -- Send message to player. - self:MessageToPlayer(playerData, text, "MARSHAL") + self:MessageToPlayer(playerData, text, "MARSHAL", "") end end @@ -9201,7 +9364,7 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) end -- Send message to player. - self:MessageToPlayer(playerData, text, "MARSHAL") + self:MessageToPlayer(playerData, text, "MARSHAL", "") end end diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 929db1163..cdec7ebad 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -237,7 +237,7 @@ RECOVERYTANKER = { --- Class version. -- @field #string version -RECOVERYTANKER.version="0.9.9" +RECOVERYTANKER.version="1.0.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -301,11 +301,12 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetPatternUpdateHeading() self:SetPatternUpdateInterval() - --[[ - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) - ]] + -- Debug trace. + if false then + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end ----------------------- --- FSM Transitions --- @@ -964,7 +965,9 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) self:T(self.lid..text) -- Respawn tanker. - self.tanker=group:RespawnAtCurrentAirbase() + --self.tanker=group:RespawnAtCurrentAirbase() + -- Delaying respawn due to DCS bug https://github.com/FlightControl-Master/MOOSE/issues/1076 + SCHEDULER:New(nil , group.RespawnAtCurrentAirbase, {group}, 1) -- Create tanker beacon and activate TACAN. if self.TACANon then @@ -972,8 +975,8 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) end -- Initial route. - SCHEDULER:New(self, self._InitRoute, {-self.distStern+UTILS.NMToMeters(3)}, 1) - --self:_InitRoute(-self.distStern+UTILS.NMToMeters(3), 1) + SCHEDULER:New(self, self._InitRoute, {-self.distStern+UTILS.NMToMeters(3)}, 2) + end end diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index dca4caab2..7a39d29d7 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -218,7 +218,7 @@ RESCUEHELO = { --- Class version. -- @field #string version -RESCUEHELO.version="0.9.9" +RESCUEHELO.version="1.0.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -269,6 +269,7 @@ function RESCUEHELO:New(carrierunit, helogroupname) self:SetAltitude() self:SetOffsetX() self:SetOffsetZ() + self:SetRespawnOn() self:SetRescueOn() self:SetRescueZone() self:SetRescueHoverSpeed() @@ -279,11 +280,12 @@ function RESCUEHELO:New(carrierunit, helogroupname) self.rtb=false self.carrierstop=false - --[[ - BASE:TraceOnOff(true) - BASE:TraceClass("RESCUEHELO") - BASE:TraceLevel(1) - ]] + -- Debug trace. + if false then + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end ----------------------- --- FSM Transitions --- @@ -656,28 +658,35 @@ function RESCUEHELO:OnEventLand(EventData) self:T(string.format("Rescue helo %s returned from rescue operation.", groupname)) - end + end + -- Check if takeoff air or respawn in air is set. Landing event should not happen unless the helo was on a rescue mission. if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then if self:IsRescuing() then self:T(string.format("Rescue helo %s returned from rescue operation.", groupname)) + + -- Respawn helo at current airbase. + self.helo=group:RespawnAtCurrentAirbase() else self:T2(string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true unless a rescue operation finished.", groupname)) + + -- Respawn helo at current airbase anyway. + if self.respawn then + self.helo=group:RespawnAtCurrentAirbase() + end end - - -- Respawn helo at current airbase anyway. - self.helo=group:RespawnAtCurrentAirbase() - else -- Respawn helo at current airbase. - self.helo=group:RespawnAtCurrentAirbase() + if self.respawn then + self.helo=group:RespawnAtCurrentAirbase() + end end @@ -782,7 +791,7 @@ function RESCUEHELO:onafterStart(From, Event, To) local dist=UTILS.NMToMeters(0.2) -- Coordinate behind the carrier. Altitude at least 100 meters for spawning because it drops down a bit. - local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg):SetAltitude(math.max(140, self.altitude)) + local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg):SetAltitude(math.max(100, self.altitude)) -- Orientation of spawned group. Spawn:InitHeading(hdg) diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 71b020489..04fd9e93f 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -238,6 +238,8 @@ AIRBASE.Normandy = { -- * AIRBASE.PersianGulf.Sharjah_Intl -- * AIRBASE.PersianGulf.Shiraz_International_Airport -- * AIRBASE.PersianGulf.Kerman_Airport +-- * AIRBASE.PersianGulf.Jiroft_Airport +-- * AIRBASE.PersianGulf.Lavan_Island_Airport -- @field PersianGulf AIRBASE.PersianGulf = { ["Fujairah_Intl"] = "Fujairah Intl", @@ -259,6 +261,8 @@ AIRBASE.PersianGulf = { ["Sharjah_Intl"] = "Sharjah Intl", ["Shiraz_International_Airport"] = "Shiraz International Airport", ["Kerman_Airport"] = "Kerman Airport", + ["Jiroft_Airport"] = "Jiroft Airport", + ["Lavan_Island_Airport"] = "Lavan Island Airport", } --- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index ae30b28a3..c1e84311d 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1609,7 +1609,7 @@ function GROUP:Respawn( Template, Reset ) -- Destroy old group. Dont trigger any dead/crash events since this is a respawn. self:Destroy(false) - self:E({Template=Template}) + self:T({Template=Template}) -- Spawn new group. _DATABASE:Spawn(Template) From ea93e3863be818413498c841dbc588967afc5192 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 26 Dec 2018 07:42:47 +0100 Subject: [PATCH 119/147] Loader --- Moose Setup/Moose_Create.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Setup/Moose_Create.lua b/Moose Setup/Moose_Create.lua index b076b8d87..69979ba86 100644 --- a/Moose Setup/Moose_Create.lua +++ b/Moose Setup/Moose_Create.lua @@ -14,7 +14,7 @@ print( "Moose target path : " .. MooseTargetPath ) local MooseSourcesFilePath = MooseSetupPath .. "/Moose.files" local LoaderFilePath = MooseTargetPath.."/Moose.lua" -local MooseFilePath = MooseTargetPath .. "/Loader.lua" +local MooseFilePath = MooseTargetPath .. "/Modules.lua" print( "Reading Moose source list : " .. MooseSourcesFilePath ) @@ -39,7 +39,7 @@ local MooseLoaderText = MooseLoader:read( "*a" ) MooseLoader:close() LoaderFile:write( MooseLoaderText ) -LoaderFile:write( "__Moose.Include( 'Scripts/Moose/Moose.lua'\n" ) +LoaderFile:write( "__Moose.Include( 'Scripts/Moose/Modules.lua' ) \n" ) local MooseSourcesFile = io.open( MooseSourcesFilePath, "r" ) local MooseSource = MooseSourcesFile:read("*l") From 610938b6e89ba82023fbf7a5eb7b684e1a7890b9 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 26 Dec 2018 09:06:16 +0100 Subject: [PATCH 120/147] Changes --- Moose Development/Moose/Modules.lua | 75 +++++++++++++++++++++++++ Moose Development/Moose/Moose.lua | 23 ++++++++ Moose Setup/Eclipse/Moose Loader.launch | 6 ++ 3 files changed, 104 insertions(+) create mode 100644 Moose Development/Moose/Modules.lua create mode 100644 Moose Development/Moose/Moose.lua create mode 100644 Moose Setup/Eclipse/Moose Loader.launch diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua new file mode 100644 index 000000000..75f82b6a3 --- /dev/null +++ b/Moose Development/Moose/Modules.lua @@ -0,0 +1,75 @@ +__Moose.Include( 'Scripts/Moose/Utilities/Routines.lua' ) +__Moose.Include( 'Scripts/Moose/Utilities/Utils.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Base.lua' ) +__Moose.Include( 'Scripts/Moose/Core/UserFlag.lua' ) +__Moose.Include( 'Scripts/Moose/Core/UserSound.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Report.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Scheduler.lua' ) +__Moose.Include( 'Scripts/Moose/Core/ScheduleDispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Event.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Settings.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Menu.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Zone.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Database.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Set.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Point.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Velocity.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Message.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Fsm.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Radio.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Spawn.lua' ) +__Moose.Include( 'Scripts/Moose/Core/SpawnStatic.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Goal.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Cargo.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Spot.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Object.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Identifiable.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Positionable.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Controllable.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Group.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Unit.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Client.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Static.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Airbase.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Scenery.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Scoring.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/CleanUp.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Movement.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Sead.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Escort.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/MissileTrainer.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/ATC_Ground.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Detection.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Designate.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/RAT.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Range.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/ZoneGoal.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/ZoneGoalCoalition.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/ZoneCaptureCoalition.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A_Patrol.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A_Cap.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A_Gci.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Patrol.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cap.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cas.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Bai.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Formation.lua' ) +__Moose.Include( 'Scripts/Moose/Actions/Act_Assign.lua' ) +__Moose.Include( 'Scripts/Moose/Actions/Act_Route.lua' ) +__Moose.Include( 'Scripts/Moose/Actions/Act_Account.lua' ) +__Moose.Include( 'Scripts/Moose/Actions/Act_Assist.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/CommandCenter.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Mission.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/TaskInfo.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/DetectionManager.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_A2G_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_A2G.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_A2A_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_A2A.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Cargo.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/TaskZoneCapture.lua' ) +__Moose.Include( 'Scripts/Moose/Globals.lua' ) diff --git a/Moose Development/Moose/Moose.lua b/Moose Development/Moose/Moose.lua new file mode 100644 index 000000000..156f8143d --- /dev/null +++ b/Moose Development/Moose/Moose.lua @@ -0,0 +1,23 @@ +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) + +local base = _G + +__Moose = {} + +__Moose.Include = function( IncludeFile ) + if not __Moose.Includes[ IncludeFile ] then + __Moose.Includes[IncludeFile] = IncludeFile + local f = assert( base.loadfile( IncludeFile ) ) + if f == nil then + error ("Moose: Could not load Moose file " .. IncludeFile ) + else + env.info( "Moose: " .. IncludeFile .. " dynamically loaded." ) + return f() + end + end +end + +__Moose.Includes = {} +__Moose.Include( 'Scripts/Moose/Modules.lua' ) +BASE:TraceOnOff( true ) +env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/Moose Setup/Eclipse/Moose Loader.launch b/Moose Setup/Eclipse/Moose Loader.launch new file mode 100644 index 000000000..006a2ce77 --- /dev/null +++ b/Moose Setup/Eclipse/Moose Loader.launch @@ -0,0 +1,6 @@ + + + + + + From 32c064a1bef346d4ff654eaea2de3ddac6579da1 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 26 Dec 2018 09:10:03 +0100 Subject: [PATCH 121/147] Remove Moose.lua --- Moose Development/Moose/Moose.lua | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 Moose Development/Moose/Moose.lua diff --git a/Moose Development/Moose/Moose.lua b/Moose Development/Moose/Moose.lua deleted file mode 100644 index 156f8143d..000000000 --- a/Moose Development/Moose/Moose.lua +++ /dev/null @@ -1,23 +0,0 @@ -env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) - -local base = _G - -__Moose = {} - -__Moose.Include = function( IncludeFile ) - if not __Moose.Includes[ IncludeFile ] then - __Moose.Includes[IncludeFile] = IncludeFile - local f = assert( base.loadfile( IncludeFile ) ) - if f == nil then - error ("Moose: Could not load Moose file " .. IncludeFile ) - else - env.info( "Moose: " .. IncludeFile .. " dynamically loaded." ) - return f() - end - end -end - -__Moose.Includes = {} -__Moose.Include( 'Scripts/Moose/Modules.lua' ) -BASE:TraceOnOff( true ) -env.info( '*** MOOSE INCLUDE END *** ' ) From 5e01db88097593caa95f2e290968f98afd970d8c Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 27 Dec 2018 07:33:51 +0100 Subject: [PATCH 122/147] New stuff --- ...der.launch => Moose Loader Dynamic.launch} | 2 +- .../Eclipse/Moose Loader Static.launch | 6 +++++ .../Moose Templates/Moose_Dynamic_Loader.lua | 2 ++ Moose Setup/Moose.lua | 22 +++++++++++++++++++ Moose Setup/Moose_Create.lua | 22 ++++++++----------- 5 files changed, 40 insertions(+), 14 deletions(-) rename Moose Setup/Eclipse/{Moose Loader.launch => Moose Loader Dynamic.launch} (91%) create mode 100644 Moose Setup/Eclipse/Moose Loader Static.launch create mode 100644 Moose Setup/Moose.lua diff --git a/Moose Setup/Eclipse/Moose Loader.launch b/Moose Setup/Eclipse/Moose Loader Dynamic.launch similarity index 91% rename from Moose Setup/Eclipse/Moose Loader.launch rename to Moose Setup/Eclipse/Moose Loader Dynamic.launch index 006a2ce77..60d840ca2 100644 --- a/Moose Setup/Eclipse/Moose Loader.launch +++ b/Moose Setup/Eclipse/Moose Loader Dynamic.launch @@ -1,6 +1,6 @@ - + diff --git a/Moose Setup/Eclipse/Moose Loader Static.launch b/Moose Setup/Eclipse/Moose Loader Static.launch new file mode 100644 index 000000000..20723f471 --- /dev/null +++ b/Moose Setup/Eclipse/Moose Loader Static.launch @@ -0,0 +1,6 @@ + + + + + + diff --git a/Moose Setup/Moose Templates/Moose_Dynamic_Loader.lua b/Moose Setup/Moose Templates/Moose_Dynamic_Loader.lua index 24ba37689..d63555297 100644 --- a/Moose Setup/Moose Templates/Moose_Dynamic_Loader.lua +++ b/Moose Setup/Moose Templates/Moose_Dynamic_Loader.lua @@ -18,3 +18,5 @@ __Moose.Include = function( IncludeFile ) end __Moose.Includes = {} + +__Moose.Include( 'Scripts/Moose/Modules.lua' ) diff --git a/Moose Setup/Moose.lua b/Moose Setup/Moose.lua new file mode 100644 index 000000000..0ecf84e0f --- /dev/null +++ b/Moose Setup/Moose.lua @@ -0,0 +1,22 @@ +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) + +local base = _G + +__Moose = {} + +__Moose.Include = function( IncludeFile ) + if not __Moose.Includes[ IncludeFile ] then + __Moose.Includes[IncludeFile] = IncludeFile + local f = assert( base.loadfile( IncludeFile ) ) + if f == nil then + error ("Moose: Could not load Moose file " .. IncludeFile ) + else + env.info( "Moose: " .. IncludeFile .. " dynamically loaded." ) + return f() + end + end +end + +__Moose.Includes = {} + +__Moose.Include( "Modules.lua" ) diff --git a/Moose Setup/Moose_Create.lua b/Moose Setup/Moose_Create.lua index 69979ba86..1b5d6a201 100644 --- a/Moose Setup/Moose_Create.lua +++ b/Moose Setup/Moose_Create.lua @@ -10,13 +10,11 @@ print( "Moose (D)ynamic (S)tatic : " .. MooseDynamicStatic ) print( "Commit Hash ID : " .. MooseCommitHash ) print( "Moose development path : " .. MooseDevelopmentPath ) print( "Moose setup path : " .. MooseSetupPath ) -print( "Moose target path : " .. MooseTargetPath ) -local MooseSourcesFilePath = MooseSetupPath .. "/Moose.files" -local LoaderFilePath = MooseTargetPath.."/Moose.lua" -local MooseFilePath = MooseTargetPath .. "/Modules.lua" +local MooseModulesFilePath = MooseDevelopmentPath .. "/Modules.lua" +local LoaderFilePath = MooseSetupPath .. "/Moose.lua" -print( "Reading Moose source list : " .. MooseSourcesFilePath ) +print( "Reading Moose source list : " .. MooseModulesFilePath ) local LoaderFile = io.open( LoaderFilePath, "w" ) @@ -32,28 +30,26 @@ if MooseDynamicStatic == "S" then MooseLoaderPath = MooseSetupPath .. "/Moose Templates/Moose_Static_Loader.lua" end -local MooseFile = io.open( MooseFilePath, "w" ) - local MooseLoader = io.open( MooseLoaderPath, "r" ) local MooseLoaderText = MooseLoader:read( "*a" ) MooseLoader:close() LoaderFile:write( MooseLoaderText ) -LoaderFile:write( "__Moose.Include( 'Scripts/Moose/Modules.lua' ) \n" ) -local MooseSourcesFile = io.open( MooseSourcesFilePath, "r" ) +local MooseSourcesFile = io.open( MooseModulesFilePath, "r" ) local MooseSource = MooseSourcesFile:read("*l") +_, _, MooseSource = string.find( MooseSource, "Scripts/Moose/(.+)'" ) + while( MooseSource ) do if MooseSource ~= "" then local MooseFilePath = MooseDevelopmentPath .. "/" .. MooseSource if MooseDynamicStatic == "D" then - print( "Load dynamic: " .. MooseSource ) - MooseFile:write( "__Moose.Include( 'Scripts/Moose/" .. MooseSource .. "' )\n" ) + print( "Load dynamic: " .. MooseFilePath ) end if MooseDynamicStatic == "S" then - print( "Load static: " .. MooseSource ) + print( "Load static: " .. MooseFilePath ) local MooseSourceFile = io.open( MooseFilePath, "r" ) local MooseSourceFileText = MooseSourceFile:read( "*a" ) MooseSourceFile:close() @@ -63,6 +59,7 @@ while( MooseSource ) do end MooseSource = MooseSourcesFile:read("*l") + _, _, MooseSource = string.find( MooseSource, "Scripts/Moose/(.+)'" ) end if MooseDynamicStatic == "D" then @@ -76,4 +73,3 @@ LoaderFile:write( "env.info( '*** MOOSE INCLUDE END *** ' )\n" ) MooseSourcesFile:close() LoaderFile:close() -MooseFile:close() From df6a10862e7a6c8224f3372d60d53aa6619718e7 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 28 Dec 2018 09:29:33 +0100 Subject: [PATCH 123/147] New dynamic loader and adapted Moose.lua creation from Modules.lua in the MOOSE root directory. --- Moose Development/Moose/Modules.lua | 42 ++++++++++++++++++- .../Eclipse/Moose Loader Dynamic.launch | 2 +- Moose Setup/Moose.lua | 22 ---------- Moose Setup/Moose_Create.lua | 3 +- 4 files changed, 44 insertions(+), 25 deletions(-) delete mode 100644 Moose Setup/Moose.lua diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 75f82b6a3..d0614bbfe 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -1,5 +1,6 @@ __Moose.Include( 'Scripts/Moose/Utilities/Routines.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/Utils.lua' ) + __Moose.Include( 'Scripts/Moose/Core/Base.lua' ) __Moose.Include( 'Scripts/Moose/Core/UserFlag.lua' ) __Moose.Include( 'Scripts/Moose/Core/UserSound.lua' ) @@ -20,8 +21,8 @@ __Moose.Include( 'Scripts/Moose/Core/Radio.lua' ) __Moose.Include( 'Scripts/Moose/Core/Spawn.lua' ) __Moose.Include( 'Scripts/Moose/Core/SpawnStatic.lua' ) __Moose.Include( 'Scripts/Moose/Core/Goal.lua' ) -__Moose.Include( 'Scripts/Moose/Core/Cargo.lua' ) __Moose.Include( 'Scripts/Moose/Core/Spot.lua' ) + __Moose.Include( 'Scripts/Moose/Wrapper/Object.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Identifiable.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Positionable.lua' ) @@ -32,6 +33,13 @@ __Moose.Include( 'Scripts/Moose/Wrapper/Client.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Static.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Airbase.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Scenery.lua' ) + +__Moose.Include( 'Scripts/Moose/Cargo/Cargo.lua' ) +__Moose.Include( 'Scripts/Moose/Cargo/CargoUnit.lua' ) +__Moose.Include( 'Scripts/Moose/Cargo/CargoSlingload.lua' ) +__Moose.Include( 'Scripts/Moose/Cargo/CargoCrate.lua' ) +__Moose.Include( 'Scripts/Moose/Cargo/CargoGroup.lua' ) + __Moose.Include( 'Scripts/Moose/Functional/Scoring.lua' ) __Moose.Include( 'Scripts/Moose/Functional/CleanUp.lua' ) __Moose.Include( 'Scripts/Moose/Functional/Movement.lua' ) @@ -46,30 +54,62 @@ __Moose.Include( 'Scripts/Moose/Functional/Range.lua' ) __Moose.Include( 'Scripts/Moose/Functional/ZoneGoal.lua' ) __Moose.Include( 'Scripts/Moose/Functional/ZoneGoalCoalition.lua' ) __Moose.Include( 'Scripts/Moose/Functional/ZoneCaptureCoalition.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Artillery.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Suppression.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/PseudoATC.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Warehouse.lua' ) + +__Moose.Include( 'Scripts/Moose/Ops/Airboss.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/RecoveryTanker.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/RescueHelo.lua' ) + __Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Air.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_A2A.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_A2A_Patrol.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_A2A_Cap.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_A2A_Gci.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_A2A_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_Engage.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_BAI.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_CAS.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_SEAD.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_Patrol.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_Dispatcher.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Patrol.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Cap.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Cas.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Bai.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Formation.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_APC.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Helicopter.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Airplane.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher_APC.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher_Airplane.lua' ) + __Moose.Include( 'Scripts/Moose/Actions/Act_Assign.lua' ) __Moose.Include( 'Scripts/Moose/Actions/Act_Route.lua' ) __Moose.Include( 'Scripts/Moose/Actions/Act_Account.lua' ) __Moose.Include( 'Scripts/Moose/Actions/Act_Assist.lua' ) + __Moose.Include( 'Scripts/Moose/Tasking/CommandCenter.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/Mission.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/Task.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/TaskInfo.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Manager.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/DetectionManager.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/Task_A2G_Dispatcher.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/Task_A2G.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/Task_A2A_Dispatcher.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/Task_A2A.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/Task_Cargo.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Cargo_Transport.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Cargo_CSAR.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Cargo_Dispatcher.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/TaskZoneCapture.lua' ) + __Moose.Include( 'Scripts/Moose/Globals.lua' ) diff --git a/Moose Setup/Eclipse/Moose Loader Dynamic.launch b/Moose Setup/Eclipse/Moose Loader Dynamic.launch index 60d840ca2..5efaf2485 100644 --- a/Moose Setup/Eclipse/Moose Loader Dynamic.launch +++ b/Moose Setup/Eclipse/Moose Loader Dynamic.launch @@ -1,6 +1,6 @@ - + diff --git a/Moose Setup/Moose.lua b/Moose Setup/Moose.lua deleted file mode 100644 index 0ecf84e0f..000000000 --- a/Moose Setup/Moose.lua +++ /dev/null @@ -1,22 +0,0 @@ -env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) - -local base = _G - -__Moose = {} - -__Moose.Include = function( IncludeFile ) - if not __Moose.Includes[ IncludeFile ] then - __Moose.Includes[IncludeFile] = IncludeFile - local f = assert( base.loadfile( IncludeFile ) ) - if f == nil then - error ("Moose: Could not load Moose file " .. IncludeFile ) - else - env.info( "Moose: " .. IncludeFile .. " dynamically loaded." ) - return f() - end - end -end - -__Moose.Includes = {} - -__Moose.Include( "Modules.lua" ) diff --git a/Moose Setup/Moose_Create.lua b/Moose Setup/Moose_Create.lua index 1b5d6a201..7d507b269 100644 --- a/Moose Setup/Moose_Create.lua +++ b/Moose Setup/Moose_Create.lua @@ -10,9 +10,10 @@ print( "Moose (D)ynamic (S)tatic : " .. MooseDynamicStatic ) print( "Commit Hash ID : " .. MooseCommitHash ) print( "Moose development path : " .. MooseDevelopmentPath ) print( "Moose setup path : " .. MooseSetupPath ) +print( "Moose target path : " .. MooseTargetPath ) local MooseModulesFilePath = MooseDevelopmentPath .. "/Modules.lua" -local LoaderFilePath = MooseSetupPath .. "/Moose.lua" +local LoaderFilePath = MooseTargetPath .. "/Moose.lua" print( "Reading Moose source list : " .. MooseModulesFilePath ) From bd25a8ccb46666a8748ef309f28961ecb8de591e Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 29 Dec 2018 23:49:52 +0100 Subject: [PATCH 124/147] Fixing too small static Moose.lua --- Moose Setup/Eclipse/Moose Loader Static.launch | 2 +- Moose Setup/Moose_Create.lua | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Moose Setup/Eclipse/Moose Loader Static.launch b/Moose Setup/Eclipse/Moose Loader Static.launch index 20723f471..4ee25e04a 100644 --- a/Moose Setup/Eclipse/Moose Loader Static.launch +++ b/Moose Setup/Eclipse/Moose Loader Static.launch @@ -1,6 +1,6 @@ - + diff --git a/Moose Setup/Moose_Create.lua b/Moose Setup/Moose_Create.lua index 7d507b269..42a3213aa 100644 --- a/Moose Setup/Moose_Create.lua +++ b/Moose Setup/Moose_Create.lua @@ -40,11 +40,10 @@ LoaderFile:write( MooseLoaderText ) local MooseSourcesFile = io.open( MooseModulesFilePath, "r" ) local MooseSource = MooseSourcesFile:read("*l") -_, _, MooseSource = string.find( MooseSource, "Scripts/Moose/(.+)'" ) - while( MooseSource ) do if MooseSource ~= "" then + MooseSource = string.match( MooseSource, "Scripts/Moose/(.+)'" ) local MooseFilePath = MooseDevelopmentPath .. "/" .. MooseSource if MooseDynamicStatic == "D" then print( "Load dynamic: " .. MooseFilePath ) @@ -60,7 +59,6 @@ while( MooseSource ) do end MooseSource = MooseSourcesFile:read("*l") - _, _, MooseSource = string.find( MooseSource, "Scripts/Moose/(.+)'" ) end if MooseDynamicStatic == "D" then From 2327fa7e4b4e4ce0e578d3c0404b6744f3d7240e Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 3 Jan 2019 00:30:03 +0100 Subject: [PATCH 125/147] AIRBOSS v0.7.0 RECOVERYTANKER v1.0.1 RESCUEHELO v1.0.1 --- Moose Development/Moose/Ops/Airboss.lua | 908 +++++++++++++----- .../Moose/Ops/RecoveryTanker.lua | 8 +- Moose Development/Moose/Ops/RescueHelo.lua | 6 +- 3 files changed, 657 insertions(+), 265 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 729e53218..cd5d21a5c 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -128,6 +128,7 @@ -- @field #boolean adinfinitum If true, carrier patrols ad infinitum, i.e. when reaching its last waypoint it starts at waypoint one again. -- @field #number magvar Magnetic declination in degrees. -- @field #number Tcollapse Last time timer.gettime() the stack collapsed. +-- @field #AIRBOSS.Recovery recoverywindow Current or next recovery window opened. -- @extends Core.Fsm#FSM --- Be the boss! @@ -294,21 +295,18 @@ -- -- ### Request Refueling -- --- If a recovery tanker was setup via the @{#AIRBOSS.SetRecoveryTanker} function, the player can request refueling. If the tanker is ready, refueling is granted and the player --- can leave the marshal stack for refueling. The stack will collapse and the player needs to request marshal again, when refueling is finished. +-- If a recovery taker has been set up via the @{#AIRBOSS.SetRecoveryTanker}, the player can request refueling at any time. If currently in the marshal stack, the stack above will collapse. +-- The player will be informed if the tanker is currently busy or going RTB to refuel itsel at its home base. Once the re-fueling is complete, the player has to re-register to the marshal stack. +-- +-- ### [Reset My Status] +-- +-- This will reset the current player status. If player is currently in a marshal stack, he will be removed from the marshal queue and the stack above will collapse. +-- The player needs to re-register later if desired. If player is currently in the landing pattern, he will be removed from the pattern queue. -- -- ## Help Menu -- -- This menu provides commands to help the player. -- --- ### Skill Level Submenu --- --- The player can choose between three skill or difficulty levels. --- --- * **Flight Student**: The player receives tips at certain stages of the pattern, e.g. if he is at the right altitude, speed, etc. --- * **Naval Aviator**: Less tips are show. Player should be familiar with the procedures and its aircraft parameters. --- * **TOPGUN Graduate**: Only very few information is provided to the player. This is for pros. --- -- ### Mark Zones Submenu -- -- These commands can be used to mark marshal or landing pattern zones. @@ -318,7 +316,15 @@ -- * **Flare Pattern Zones** Similar to smoke but uses flares to mark the pattern zones. -- * **Smoke Marshal Zone** This smokes the surrounding area of the currently assigned Marshal zone of the player. Player has to be registered in Marshal queue. -- * **Flare Marshal Zone** Similar to smoke but uses flares to mark the Marshal zone. --- +-- +-- ### Skill Level Submenu +-- +-- The player can choose between three skill or difficulty levels. +-- +-- * **Flight Student**: The player receives tips at certain stages of the pattern, e.g. if he is at the right altitude, speed, etc. +-- * **Naval Aviator**: Less tips are show. Player should be familiar with the procedures and its aircraft parameters. +-- * **TOPGUN Graduate**: Only very few information is provided to the player. This is for the pros. +-- -- ### My Status -- -- This command provides information about the current player status. For example, his current step in the pattern. @@ -336,11 +342,6 @@ -- -- Marshal will transmit a short message on his radio frequency. See @{#AIRBOSS.SetMarshalRadio}. -- --- ### [Reset My Status] --- --- This will reset the current player status. If player is currently in a marshal stack, he will be removed from the marshal queue and the stack will collapse. --- The player needs to re-register later if desired. If player is currently in the landing pattern, he will be removed from the pattern queue. --- -- ## Kneeboard Menu -- -- The Kneeboard menu provides information about the carrier, weather and player results. @@ -381,17 +382,18 @@ -- -- LSO grading starts when the player enters the groove. The flight path and aircraft attitude is evaluated at certain steps -- --- * **X** At the Start --- * **IM** In the Middle --- * **IC** In Close --- * **AR** At the Ramp --- * **IW** In the Wiress --- +-- * **X** At the Start (0.75 NM = 1389 m from the rundown). +-- * **IM** In the Middle (0.375 NM = 695 from the rundown). +-- * **IC** In Close (0.18 NM = 333 m from the rundown). +-- * **AR** At the Ramp (0.027 NM = 50 m from the rundown). +-- * **IW** In the Wiress (at the landing position). +-- -- Grading at each step includes the above calls, i.e. -- -- * **L**ined **U**p **L**eft or **R**ight: LUL, LUR --- * Too **H**igh or too **L**ow: H, L +-- * Too **H**igh or too **LO**w: H, LO -- * Too **F**ast or too **SLO**w: F, SLO +-- * Fly through **down* or **up**: \\, / -- -- Each grading, x, is subdivided by -- @@ -400,7 +402,14 @@ -- -- The position at the landing event is analyzed and the corresponding trapped wire calculated. If no wire was caught, the LSO will give the bolter call. -- --- If a player is sigifiantly off from the ideal parameters in close or at the ramp, the LSO will wave the player off. +-- If a player is sigifiantly off from the ideal parameters from IC to AR, the LSO will wave the player off. Thresholds for wave off are +-- +-- * Line up error > 3.0 degrees left or right and/or +-- * Glide slope error < -1.2 degrees or > 1.8 degrees and/or +-- * AOA depending on aircraft type and only applied if skill level is "TOPGUN graduate". +-- +-- Line up and glide slope error thresholds were tested using [https://forums.eagle.ru/showthread.php?t=211557](VFA-113 Stingers LSO Mod), +-- if the aircraft is outside the red box. -- -- ## Pattern Wave Off -- @@ -544,6 +553,7 @@ AIRBOSS = { adinfinitum = nil, magvar = nil, Tcollapse = nil, + recoverywindow = nil, } --- Player aircraft types capable of landing on carriers. @@ -637,7 +647,6 @@ AIRBOSS.CarrierType={ -- @field #string WAKE "Wake". -- @field #string FINAL "Final". -- @field #string GROOVE_XX "Groove X". --- @field #string GROOVE_RB "Groove Roger Ball". -- @field #string GROOVE_IM "Groove In the Middle". -- @field #string GROOVE_IC "Groove In Close". -- @field #string GROOVE_AR "Groove At the Ramp". @@ -663,7 +672,6 @@ AIRBOSS.PatternStep={ WAKE="Wake", FINAL="Turn Final", GROOVE_XX="Groove X", - GROOVE_RB="Groove Roger Ball", GROOVE_IM="Groove In the Middle", GROOVE_IC="Groove In Close", GROOVE_AR="Groove At the Ramp", @@ -671,6 +679,23 @@ AIRBOSS.PatternStep={ DEBRIEF="Debrief", } +--- Groove position. +-- @type AIRBOSS.GroovePos +-- @field #string X0 "X0": Entering the groove. +-- @field #string XX "XX": At the start, i.e. 3/4 from the run down. +-- @field #string IM "IM": In the middle. +-- @field #string IC "IC": In close. +-- @field #string AR "AR": At the ramp. +-- @field #string IW "IW": In the wires. +AIRBOSS.GroovePos={ + X0="X0", + XX="XX", + IM="IM", + IC="IC", + AR="AR", + IW="IW", +} + --- Radio sound file and subtitle. -- @type AIRBOSS.RadioCall -- @field #string file Sound file name without suffix. @@ -692,7 +717,7 @@ AIRBOSS.PatternStep={ -- @field #AIRBOSS.RadioCall PADDLESCONTACT "Paddles, contact" call. -- @field #AIRBOSS.RadioCall CALLTHEBALL "Call the Ball" -- @field #AIRBOSS.RadioCall ROGERBALL "Roger ball" call. --- @field #AIRBOSS.RadioCall WAVEOFF "Wave off" call +-- @field #AIRBOSS.RadioCall WAVEOFF "Wave off" call. -- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call -- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove" call. -- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call. @@ -1022,25 +1047,6 @@ AIRBOSS.Difficulty={ -- @field #boolean OPEN Recovery window is currently open. -- @field #boolean OVER Recovery window is over and closed. ---- Groove position. --- @type AIRBOSS.GroovePos --- @field #string X0 Entering the groove. --- @field #string XX At the start, i.e. 3/4 from the run down. --- @field #string RB Roger ball. --- @field #string IM In the middle. --- @field #string IC In close. --- @field #string AR At the ramp. --- @field #string IW In the wires. -AIRBOSS.GroovePos={ - X0="X0", - XX="X", - RB="RB", - IM="IM", - IC="IC", - AR="AR", - IW="IW", -} - --- Groove data. -- @type AIRBOSS.GrooveData -- @field #number Step Current step. @@ -1051,6 +1057,7 @@ AIRBOSS.GroovePos={ -- @field #number Roll Roll angle. -- @field #number Rhdg Relative heading player to carrier. 0=parallel, +-90=perpendicular. -- @field #number TGroove Time stamp when pilot entered the groove. +-- @field #string FlyThrough Fly through up "/" or fly through down "\". --- LSO grade -- @type AIRBOSS.LSOgrade @@ -1091,6 +1098,7 @@ AIRBOSS.GroovePos={ -- @field #boolean holding If true, flight is in holding zone. -- @field #boolean ballcall If true, flight called the ball in the groove. -- @field #table elements Flight group elements. +-- @field #number Tcharlie Charlie (abs) time in seconds. --- Parameters of an element in a flight group. -- @type AIRBOSS.FlightElement @@ -1122,7 +1130,8 @@ AIRBOSS.GroovePos={ -- @field #number Tgroove Time in the groove in seconds. -- @field #number wire Wire caught by player when trapped. -- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. --- @field #table menu F10 radio menu +-- @field #table points Points of passes until finally landed. +-- @field #number finalscore Final score if points are averaged over multiple passes. -- @extends #AIRBOSS.FlightGroup --- Main radio menu. @@ -1131,21 +1140,22 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.6.3" +AIRBOSS.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Check player heading at zones, e.g. initial. -- TODO: Include recovery tanker into next stack calculation. Angels six should be empty. --- TODO: Get charly time estimate function. -- TODO: Player eject and crash debrief "gradings". -- TODO: Subtitles off options on player level. -- TODO: PWO during case 2/3. Also when too close to other player. -- TODO: Option to filter AI groups for recovery. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! -- TODO: Persistence of results. +-- DONE: Get Charlie time estimate function. +-- DONE: Average player grades until landing. +-- DONE: Check player heading at zones, e.g. initial. -- DONE: Fix bug that player leaves the approach zone if he boltered or was waved off during Case II or III. NOTE: Partly due to increasing approach zone size. -- DONE: Fix bug that player gets an altitude warning if stack collapses. NOTE: Would not work if two stacks Case I and II/III are used. -- DONE: Improve radio messages. Maybe usersound for messages which are only meant for players? @@ -1211,14 +1221,6 @@ function AIRBOSS:New(carriername, alias) self:E(text) return nil end - - -- Debug trace. - if false then - self.Debug=true - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) - end -- Set some string id for output to DCS.log file. self.lid=string.format("AIRBOSS %s | ", carriername) @@ -1304,6 +1306,18 @@ function AIRBOSS:New(carriername, alias) -- CASE I/II moving zone: Zone 2.75 NM astern and 0.1 NM starboard of the carrier with a diameter of 1 NM. self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, UTILS.NMToMeters(0.5), {dx=-UTILS.NMToMeters(2.75), dy=UTILS.NMToMeters(0.1), relative_to_unit=true}) + + ------------------- + -- Debug Section -- + ------------------- + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end -- Smoke zones. if self.Debug and false then @@ -1874,8 +1888,8 @@ function AIRBOSS:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Crash) self:HandleEvent(EVENTS.Ejection) - -- Time stamp for checking queues. - self.Tqueue=timer.getTime() + -- Time stamp for checking queues. We substract 60 seconds so the routine is called right after status is called the first time. + self.Tqueue=timer.getTime()-60 -- Schedule radio queue checks. self.RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQLSO, "LSO"}, 1, 0.01) @@ -2148,6 +2162,7 @@ function AIRBOSS:_CheckRecoveryTimes() -- Next recovery case in the future. local nextwindow=nil --#AIRBOSS.Recovery + local currwindow=nil --#AIRBOSS.Recovery -- Loop over all slots. for _,_recovery in pairs(self.recoverytimes) do @@ -2177,6 +2192,9 @@ function AIRBOSS:_CheckRecoveryTimes() recovery.OPEN=true end + -- Set current recovery window. + currwindow=recovery + else -- Stop time has passed. if self:IsRecovering() and not recovery.OVER then @@ -2216,6 +2234,8 @@ function AIRBOSS:_CheckRecoveryTimes() -- Debug output. self:T(self.lid..text) + self.recoverywindow=nil + -- Carrier is idle. We need to make sure that incoming flights get the correct recovery info of the next window. if self:IsIdle() then -- Check if there is a next windows defined. @@ -2223,12 +2243,21 @@ function AIRBOSS:_CheckRecoveryTimes() -- Set case and offset of the next window. self.case=nextwindow.CASE self.holdingoffset=nextwindow.OFFSET + self.recoverywindow=nextwindow else -- No next window. Set default values. self.case=self.defaultcase self.holdingoffset=self.defaultoffset end + else + if currwindow then + self.recoverywindow=currwindow + else + self.recoverywindow=nextwindow + end end + + self:T({"FF", recoverywindow=self.recoverywindow}) end @@ -2574,7 +2603,7 @@ function AIRBOSS:_InitStennis() self.BreakEntry.name="Break Entry" self.BreakEntry.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. self.BreakEntry.Xmax= nil - self.BreakEntry.Zmin=-400 -- Not more than 400 m port of boat. Otherwise miss the zone. + self.BreakEntry.Zmin=-400 -- Not more than 400 m port of boat. Otherwise miss the zone. self.BreakEntry.Zmax=UTILS.NMToMeters(1.5) -- Not more than 1.5 NM starboard. self.BreakEntry.LimitXmin=0 -- Check and next step when at carrier and starboard of carrier. self.BreakEntry.LimitXmax=nil @@ -2605,10 +2634,10 @@ function AIRBOSS:_InitStennis() -- Abeam position. self.Abeam.name="Abeam Position" - self.Abeam.Xmin= nil - self.Abeam.Xmax= nil + self.Abeam.Xmin=-UTILS.NMToMeters(5) -- Not more then 5 NM astern of boat. Should be LIG call anyway. + self.Abeam.Xmax= UTILS.NMToMeters(5) -- Not more then 5 NM ahead of boat. self.Abeam.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. - self.Abeam.Zmax= 100 -- Must be port! + self.Abeam.Zmax= 200 -- Not more than 200 m starboard. Must be port! self.Abeam.LimitXmin=-200 -- Check and next step 200 meters behind the ship. self.Abeam.LimitXmax= nil self.Abeam.LimitZmin= nil @@ -2856,20 +2885,28 @@ end -- @return #AIRBOSS.FlightGroup Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready. function AIRBOSS:_GetNextMarshalFight() - -- Min 5 min in marshal before send to landing pattern. - local TmarshalMin=10*60 - + -- Loop over all marshal flights. for _,_flight in pairs(self.Qmarshal) do local flight=_flight --#AIRBOSS.FlightGroup -- Current stack. local stack=flight.flag:Get() - -- Marshal time. + -- Total marshal time in seconds. local Tmarshal=timer.getAbsTime()-flight.time + -- Min time in marshal stack. + local TmarshalMin=2*60 + if flight.ai then + -- Three minutes for AI. + TmarshalMin=3*60 + else + -- Two minutes for human pilots. + TmarshalMin=2*60 + end + -- Check if conditions are right. - if stack==1 and flight.holding and Tmarshal>=TmarshalMin then + if stack==1 and flight.holding~=nil and Tmarshal>=TmarshalMin then return flight end end @@ -2923,13 +2960,14 @@ function AIRBOSS:_CheckQueue() -- Min time in pattern before next aircraft is allowed. local TpatternMin if pcase==1 then - TpatternMin=3*60*npunits --45*npunits -- 45 seconds interval per plane! + TpatternMin=1*60*npunits --45*npunits -- 45 seconds interval per plane! else - TpatternMin=3*60*npunits --120*npunits -- 120 seconds interval per plane! + TpatternMin=2*60*npunits --120*npunits -- 120 seconds interval per plane! end -- Check recovery window open and enough space to last pattern flight. if self:IsRecovering() and Tpattern>TpatternMin then + self:T(string.format("Sending marshal flight %s to pattern.", marshalflight.groupname)) self:_CheckCollapseMarshalStack(marshalflight) end @@ -3355,6 +3393,99 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) return altitude, p1, p2 end +--- Calculate an estimate of the charlie time of the player based on how many other aircraft are in the marshal or pattern queue before him. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flightgroup Flight data. +-- @return #number Charlie (abs) time in seconds. Or nil, if stack<0 or no recovery window will open. +function AIRBOSS:_GetCharlieTime(flightgroup) + + -- Get current stack of player. + local stack=flightgroup.flag:Get() + + -- Flight is not in marshal stack. + if stack<=0 then + return nil + end + + -- Current abs time. + local Tnow=timer.getAbsTime() + + -- Time the player has to spend in marshal stack until all lower stacks are emptied. + local Tcharlie=0 + + local Trecovery=0 + if self.recoverywindow then + -- Time in seconds until the next recovery starts or 0 if window is already open. + Trecovery=math.max(self.recoverywindow.START-Tnow, 0) + else + return nil + end + + -- Loop over flights currently in the marshal queue. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Stack of marshal flight. + local mstack=flight.flag:Get() + + -- Time to get to the marshal stack if not holding already. + local Tarrive=0 + + -- Minimum holding time per stack. + local Tholding=3*60 + + if stack>0 and mstack>0 and mstack<=stack then + + -- Check if flight is already holding or just on its way. + if flight.holding==nil then + -- Flight is also on its way to the marshal stack. + + -- TODO: Distance depends on case. + local d0=self:GetCoordinate():Get2DDistance(flight.group:GetCoordinate()) + + -- Current velocity. + local v0=flight.group:GetVelocityMPS() + + -- Time to get to the carrier. + Tarrive=d0/v0 + + self:T3(self.lid..string.format("Tarrive=%.1f seconds, Clock %s", Tarrive, UTILS.SecondsToClock(Tnow+Tarrive))) + + else + -- Flight is already holding. + + -- Next in line. + if mstack==1 then + + -- Current holding time. flight.time stamp should be when entering holding or last time the stack collapsed. + local tholding=timer.getAbsTime()-flight.time + + -- Deduce current holding time. Ensure that is >=0. + Tholding=math.max(3*60-tholding, 0) + end + + end + + -- This is the approx time needed to get to the pattern. If we are already there, it is the time until the recovery window opens or 0 if it is already open. + local Tmin=math.max(Tarrive, Trecovery) + + -- Charlie time + 2 min holding in stack 1. + Tcharlie=math.max(Tmin, Tcharlie)+Tholding + end + + end + + -- Convert to abs time. + Tcharlie=Tcharlie+Tnow + + -- Debug info. + local text=string.format("Charlie time for flight %s (%s) %s", flightgroup.onboard, flightgroup.groupname, UTILS.SecondsToClock(Tcharlie)) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + return Tcharlie +end + --- Add a flight group to a specific marshal stack and to the marshal queue. -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight group. @@ -3367,6 +3498,9 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) -- Set recovery case. flight.case=self.case + -- Add to marshal queue. + table.insert(self.Qmarshal, flight) + -- Pressure. local P=UTILS.hPa2inHg(self:GetCoordinate():GetPressure()) @@ -3374,23 +3508,29 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case)) local brc=self:GetBRC() + -- Get charlie time estimate. + flight.Tcharlie=self:_GetCharlieTime(flight) + + -- Convert to clock string. + local Ccharlie=UTILS.SecondsToClock(flight.Tcharlie) + -- Marshal message. - -- TODO: Get charlie time estimate. - local text=string.format("Case %d, BRC is %03d, hold at %d. Expected Charlie Time XX.\n", flight.case, brc, alt) + local text=string.format("Case %d, BRC is %03d°, hold at %d. Expected Charlie Time %s.\n", flight.case, brc, alt, tostring(Ccharlie)) text=text..string.format("Altimeter %.2f. Report see me.", P) -- Hint about TACAN bearing. if self.TACANon and (not flight.ai) and flight.difficulty==AIRBOSS.Difficulty.EASY then - -- Get inverse magnetic radial potential offset. + -- Get inverse magnetic radial potential offset. local radial=self:GetRadial(flight.case, true, true, true) - text=text..string.format("\nSelect TACAN %d°, channel %d%s (%s)", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + if flight.case==1 then + -- For case 1 we want the BRC but above routine return FB. + radial=self:GetBRC() + end + text=text..string.format("\nSelect TACAN %03d°, channel %d%s (%s)", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) end -- Message to all players. self:MessageToAll(text, "MARSHAL", flight.onboard) - - -- Add to marshal queue. - table.insert(self.Qmarshal, flight) end --- Check if marshal stack can be collapsed. @@ -3467,7 +3607,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) else - -- Decrease stack/flag. Human player needs to take care himself. + -- Decrease stack/flag. Human player needs to take care himself. mflight.flag:Set(newstack) -- Inform players. @@ -3479,6 +3619,9 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) self:MessageToPlayer(mflight, text, "MARSHAL") end + + -- Set time stamp. + mflight.time=timer.getAbsTime() -- Loop over section members. for _,_sec in pairs(mflight.section) do @@ -3487,6 +3630,9 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- Also decrease flag for section members of flight. sec.flag:Set(newstack) + -- Set new time stamp. + sec.time=timer.getAbsTime() + -- Inform section member. if sec.difficulty~=AIRBOSS.Difficulty.HARD then local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(newstack, case)) @@ -3791,6 +3937,9 @@ function AIRBOSS:_NewPlayer(unitname) -- Set difficulty level. playerData.difficulty=playerData.difficulty or self.defaultskill + + -- Points rewarded. + playerData.points={} -- Init stuff for this round. playerData=self:_InitPlayer(playerData) @@ -3810,7 +3959,7 @@ end function AIRBOSS:_InitPlayer(playerData, step) self:T(self.lid..string.format("Initializing player data for %s callsign %s.", playerData.name, playerData.callsign)) - playerData.step=step or AIRBOSS.PatternStep.UNDEFINED + playerData.step=step or AIRBOSS.PatternStep.UNDEFINED playerData.groove={} playerData.debrief={} playerData.warning=nil @@ -3823,6 +3972,7 @@ function AIRBOSS:_InitPlayer(playerData, step) playerData.Tlso=timer.getTime() playerData.Tgroove=nil playerData.wire=nil + --TODO: set flag to -100 here? -- Set us up on final if group name contains "Groove". But only for the first pass. if playerData.group:GetName():match("Groove") and playerData.passes==0 then @@ -4272,7 +4422,6 @@ function AIRBOSS:_CheckPlayerStatus() self:_Final(playerData) elseif playerData.step==AIRBOSS.PatternStep.GROOVE_XX or - playerData.step==AIRBOSS.PatternStep.GROOVE_RB or playerData.step==AIRBOSS.PatternStep.GROOVE_IM or playerData.step==AIRBOSS.PatternStep.GROOVE_IC or playerData.step==AIRBOSS.PatternStep.GROOVE_AR or @@ -4284,7 +4433,7 @@ function AIRBOSS:_CheckPlayerStatus() elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then -- Debriefing in 10 seconds. - SCHEDULER:New(self, self._Debrief, {playerData}, 10) + SCHEDULER:New(nil, self._Debrief, {self, playerData}, 10) -- Undefined status. playerData.step=AIRBOSS.PatternStep.UNDEFINED @@ -4460,7 +4609,7 @@ function AIRBOSS:OnEventLand(EventData) playerData.step=AIRBOSS.PatternStep.UNDEFINED -- Call trapped function in 1 second to make sure we did not bolter. - SCHEDULER:New(self, self._Trapped, {playerData}, 1) + SCHEDULER:New(nil, self._Trapped, {self, playerData}, 1) end @@ -4589,10 +4738,13 @@ function AIRBOSS:_Holding(playerData) -- Check if stack just collapsed and give the player one minute to change the alitude. local justcollapsed=false - if self.Tcollapse then + if self.Tcollapse then -- Time since last stack change. local dT=timer.getTime()-self.Tcollapse + -- TODO: check if this works. + --local dT=timer.getAbsTime()-playerData.time + -- Check if less then 60 seconds. if dT<=60 then justcollapsed=true @@ -4895,7 +5047,7 @@ function AIRBOSS:_ArcInTurn(playerData) if self.holdingoffset<0 then turn="left" end - hint=hint..string.format("\nTurn %s and select TACAN %d°.", turn, radial) + hint=hint..string.format("\nTurn %s and select TACAN %03d°.", turn, radial) end -- Message to player. @@ -5395,6 +5547,7 @@ function AIRBOSS:_Final(playerData) groovedata.Roll=roll groovedata.Rhdg=relhead groovedata.TGroove=timer.getTime() + groovedata.FlyThrough=nil -- TODO: could add angled approach if lineup<5 and relhead>5. This would mean the player has not turned in correctly! @@ -5445,17 +5598,22 @@ function AIRBOSS:_Groove(playerData) -- Get AoA. local AoA=playerData.unit:GetAoA() - -- For debugging. - MESSAGE:New(string.format("%s: LineUp=%.1f GlideSlope=%.1f AoA=%.1f", playerData.step, lineupError, glideslopeError, AoA), 3, nil, true):ToAllIf(self.Debug) + -- Aircraft is behind the carrier. + local astern=X=RAR and rho=3 and playerData.waveoff==false then - - -- LSO call if necessary. - self:_LSOadvice(playerData, glideslopeError, lineupError) + -- Between IC and AR check for wave off. + if rho>=RAR and rho<=RIC and not playerData.waveoff then + + -- Check if player should wave off. + local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) + + -- Let's see.. + if waveoff then + + -- Debug info. + self:T3(self.lid..string.format("Waveoff distance rho=%.1f m", rho)) + + -- LSO Wave off! + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WAVEOFF) + playerData.Tlso=timer.getTime() + + -- Player was waved off! + playerData.waveoff=true + + -- Nothing else necessary. + return + end end - -------------------------------------------------------- - --- Some time here the landing event MIGHT be triggered. - -------------------------------------------------------- + ----------------- + -- Groove Data -- + ----------------- + + -- Get groove step short hand of the previous step. + local gs=self:_GS(playerData.step, -1) + + -- For debugging. + local text=string.format("Groove %s: LineUp=%.2f GlideSlope=%.2f AoA=%.2f\n", gs, lineupError, glideslopeError, AoA) + text=text..string.format("R=%.1f m, h=%.1f m", rho, alt-self.carrierparam.deckheight-2) + MESSAGE:New(text, 1, nil, true):ToAllIf(self.Debug) + + -- Check if we are beween 3/4 NM and end of ship. + if rho>=RAR and rhomath.abs(gd.LUE) then + self:T(self.lid..string.format("Got bigger Linue up error at %s: LUE %.3f>%.3f.", gs, lineupError, gd.LUE)) + gd.LUE=lineupError + end + + -- Fly through good window of glide slope. + if gd.GSE>0.4 and glideslopeError<-0.3 then + -- Fly through down ==> "\" + gd.FlyThrough="\\" + self:T(self.lid..string.format("Got Fly through DOWN at %s. Max GSE=%.1f, lower GSE=%.1f", gs, gd.GSE, glideslopeError)) + elseif gd.GSE<-0.3 and glideslopeError>0.4 then + -- Fly through up ==> "/" + gd.FlyThrough="/" + self:T(self.lid..string.format("Got Fly through UP at %s. Min GSE=%.1f, lower GSE=%.1f", gs, gd.GSE, glideslopeError)) + end + + -- Update max deviation of glide slope error. + if math.abs(glideslopeError)>math.abs(gd.GSE) then + self:T(self.lid..string.format("Got bigger glide slope error at %s: GSE |%.3f|>|%.3f|.", gs, glideslopeError, gd.GSE)) + gd.GSE=glideslopeError + end + + -- Get current AoA. + local aoa=playerData.unit:GetAoA() + + -- Get aircraft AoA parameters. + local aircraftaoa=self:_GetAircraftAoA(playerData) + + -- On Speed AoA. + local aoaopt=aircraftaoa.OnSpeed + + -- Compare AoAs wrt on speed AoA and update max deviation. + if math.abs(aoa-aoaopt)>math.abs(gd.AoA-aoaopt) then + self:T(self.lid..string.format("Got bigger AoA error at %s: AoA %.3f>%.3f.", gs, aoa, gd.AoA)) + gd.AoA=aoa + end + + end + + --------------- + -- LSO Calls -- + --------------- + + -- Time since last LSO call. + local deltaT=timer.getTime()-playerData.Tlso + + -- LSO call if necessary. + if deltaT>=3 then + self:_LSOadvice(playerData, glideslopeError, lineupError) + end + + end + + ---------------------------------------------------------- + --- Some time here the landing event MIGHT be triggered -- + ---------------------------------------------------------- -- Player infront of the carrier X>~77 m. if X>self.carrierparam.totlength+self.carrierparam.sterndist then @@ -5633,8 +5846,13 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) local waveoff=false -- Too high or too low? - if math.abs(glideslopeError)>1 then - local text=string.format("Wave off due to glide slope error |%.1f| > 1 degree!", glideslopeError) + if glideslopeError>1.8 then + local text=string.format("Wave off due to glide slope error %.2f > 1.8 degrees!", glideslopeError) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + elseif glideslopeError<-1.2 then + local text=string.format("Wave off due to glide slope error %.2f < -1.2 degrees!", glideslopeError) self:T(self.lid..string.format("%s: %s", playerData.name, text)) self:_AddToDebrief(playerData, text) waveoff=true @@ -5653,13 +5871,13 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) -- Get aircraft specific AoA values local aoaac=self:_GetAircraftAoA(playerData) -- Check too slow or too fast. - if AoAaoaac.Slow then - local text=string.format("Wave off due to AoA %.1f > %.1f!", AoA, aoaac.Slow) + elseif AoA>aoaac.SLOW then + local text=string.format("Wave off due to AoA %.1f > %.1f!", AoA, aoaac.SLOW) self:T(self.lid..string.format("%s: %s", playerData.name, text)) self:_AddToDebrief(playerData, text) waveoff=true @@ -5801,7 +6019,7 @@ function AIRBOSS:_Trapped(playerData) -- Call this function again until v < threshold. Player comes to a standstill ==> Get wire! if v>5 then - SCHEDULER:New(self, self._Trapped, {playerData}, 0.1) + SCHEDULER:New(nil, self._Trapped, {self, playerData}, 0.1) return end @@ -6178,13 +6396,17 @@ function AIRBOSS:_GetZoneHolding(case, stack) -- Get current carrier heading. local hdg=self:GetHeading() - -- Zone 2.5 NM port of carrier with a radius of 2.75 NM (holding pattern should be < 5 NM but we allow 10% error). - local R=UTILS.NMToMeters(2.5) + -- Distance to the post. + local D=UTILS.NMToMeters(2.5) - -- Create zone. - local coord=self:GetCoordinate():Translate(R, hdg+270) + -- Radius of the zone. Diameter 5 NM +10% error margin. + local R=D*1.1 - zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", coord:GetVec2(), R*1.1) + -- Post 2.5 NM port of carrier. + local Post=self:GetCoordinate():Translate(D, hdg+270) + + -- Create holding zone. + zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", Post:GetVec2(), R) else -- CASE II/II @@ -6248,8 +6470,8 @@ function AIRBOSS:_AttitudeMonitor(playerData) text=text..string.format("R=%.1f NM | X=%d Z=%d m\n", UTILS.MetersToNM(rho), dx, dz) text=text..string.format("Gamma=%.1f°", relhead) -- If in the groove, provide line up and glide slope error. - if playerData.step==AIRBOSS.PatternStep.GROOVE_XX or - playerData.step==AIRBOSS.PatternStep.GROOVE_RB or + if playerData.step==AIRBOSS.PatternStep.GROOVE_X0 or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or playerData.step==AIRBOSS.PatternStep.GROOVE_IM or playerData.step==AIRBOSS.PatternStep.GROOVE_IC or playerData.step==AIRBOSS.PatternStep.GROOVE_AR or @@ -6281,7 +6503,8 @@ function AIRBOSS:_Glideslope(unit, optangle) -- Ideally we want to land between 2nd and 3rd wire. if self.carrierparam.wire3 then - local d23=self.carrierparam.wire2 --+0.5*(self.carrierparam.wire3-self.carrierparam.wire2) + -- We take the position of the 3rd wire to approximately account for the length of the aircraft. + local d23=self.carrierparam.wire3 --self.carrierparam.wire2+0.5*(self.carrierparam.wire3-self.carrierparam.wire2) stern=stern:Translate(d23, self:GetFinalBearing(false), true) end @@ -6289,7 +6512,7 @@ function AIRBOSS:_Glideslope(unit, optangle) local x=unit:GetCoordinate():Get2DDistance(stern) -- Altitude of unit corrected by the deck height of the carrier. - local h=unit:GetAltitude()-self.carrierparam.deckheight + local h=unit:GetAltitude()-self.carrierparam.deckheight-4 -- Glide slope. local glideslope=math.atan(h/x) @@ -6297,7 +6520,8 @@ function AIRBOSS:_Glideslope(unit, optangle) -- Glide slope (error) in degrees. local gs=math.deg(glideslope)-optangle - --env.info(string.format("FF Glide slope error = %.1f, x=%.1f h=%.1f", gs, x, h)) + -- Debug. + self:T3(self.lid..string.format("Glide slope error = %.1f, x=%.1f h=%.1f", gs, x, h)) return gs end @@ -6308,9 +6532,19 @@ end -- @param #boolean runway If true, include angled runway. -- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. function AIRBOSS:_Lineup(unit, runway) + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + -- Ideally we want to land between 2nd and 3rd wire. + if self.carrierparam.wire3 then + -- We take the position of the 3rd wire to approximately account for the length of the aircraft. + local d23=self.carrierparam.wire3 --+0.5*(self.carrierparam.wire3-self.carrierparam.wire2) + stern=stern:Translate(d23, self:GetFinalBearing(false), true) + end -- Vector to carrier. - local A=self:_GetSternCoord():GetVec3() + local A=stern:GetVec3() -- Vector to player. local B=unit:GetVec3() @@ -6356,10 +6590,8 @@ function AIRBOSS:_Lineup(unit, runway) -- Current line up and error wrt to final heading of the runway. local lineup=math.deg(math.atan2(c.z, c.x)) - - --env.info(string.format("FF lineup 2 = %.1f", lineup)) - - return lineup + + return lineup end --- Get true (or magnetic) heading of carrier. @@ -6604,23 +6836,24 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- Advice time. local advice=0 + local text="" -- Glideslope high/low calls. - local text="" - if glideslopeError>1 then + --TODO: introduce GSE enumerator values. + if glideslopeError>1.5 then -- "You're high!" self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.HIGH, true) advice=advice+AIRBOSS.LSOCall.HIGH.duration - elseif glideslopeError>0.5 then - -- "You're a little high." + elseif glideslopeError>0.8 then + -- "You're high." self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.HIGH, false) advice=advice+AIRBOSS.LSOCall.HIGH.duration - elseif glideslopeError<-1.0 then + elseif glideslopeError<-0.9 then -- "Power!" self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.POWER, true) advice=advice+AIRBOSS.LSOCall.POWER.duration - elseif glideslopeError<-0.5 then - -- "You're a little low." + elseif glideslopeError<-0.6 then + -- "Power." self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.POWER, false) advice=advice+AIRBOSS.LSOCall.POWER.duration else @@ -6631,6 +6864,7 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) text=text.."\n" -- Lineup left/right calls. + -- TODO: introduce LUE enumerator values. if lineupError<-3 then -- "Come left!" self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.COMELEFT, true) @@ -6654,36 +6888,42 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) text=text..string.format(" Lineup Error = %.1f°\n", lineupError) -- Get current AoA. - local aoa=playerData.unit:GetAoA() + local AOA=playerData.unit:GetAoA() -- Get aircraft AoA parameters. - local aircraftaoa=self:_GetAircraftAoA(playerData) - - -- Rate aoa. - if aoa>=aircraftaoa.Slow then + local acaoa=self:_GetAircraftAoA(playerData) + + -- Speed via AoA. + if AOA>acaoa.SLOW then -- "Your're slow!" self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.SLOW, true) advice=advice+AIRBOSS.LSOCall.SLOW.duration - elseif aoa>=aircraftaoa.OnSpeedMax and aoaacaoa.Slow then + -- "Your're slow." self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.SLOW, false) advice=advice+AIRBOSS.LSOCall.SLOW.duration - elseif aoa>=aircraftaoa.OnSpeedMin and aoa=aircraftaoa.Fast and aoaacaoa.OnSpeedMax then + -- No call. + --S=little("SLO") + elseif AOA return empty string. if fdata==nil then @@ -6858,7 +7098,7 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) -- Aircraft specific AoA values. local acaoa=self:_GetAircraftAoA(playerData) - -- Speed. + -- Speed via AoA. Depends on aircraft type. local S=nil if AOA>acaoa.SLOW then S=underline("SLO") @@ -6874,23 +7114,25 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) S=little("F") end - -- Glideslope/altitude. Good [-0.25, 0.25] + -- Glideslope/altitude. Good [-0.3, 0.4] asymmetric! + -- TODO: introduce enumerator with GSE values. local A=nil - if GSE>1 then + if GSE>1.5 then A=underline("H") - elseif GSE>0.5 then + elseif GSE>0.8 then A="H" - elseif GSE>0.25 then + elseif GSE>0.4 then A=little("H") - elseif GSE<-1 then + elseif GSE<-0.9 then A=underline("LO") - elseif GSE<-0.5 then + elseif GSE<-0.6 then A="LO" - elseif GSE<-0.25 then + elseif GSE<-0.3 then A=little("LO") end -- Line up. Good [-0.5, 0.5] + -- TODO: introduce enumerator with LUE values. local D=nil if LUE>3 then D=underline("LUL") @@ -6909,14 +7151,21 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) -- Compile. local G="" local n=0 + -- Fly trough. + if fdata.FlyThrough then + G=G..fdata.FlyThrough + end + -- Speed. if S then G=G..S n=n+1 end + -- Glide slope. if A then G=G..A n=n+1 end + -- Line up. if D then G=G..D n=n+1 @@ -6934,7 +7183,7 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) text=text..string.format("AOA=%.1f\n",AOA) text=text..string.format("GSE=%.1f\n",GSE) text=text..string.format("LUE=%.1f\n",LUE) - text=text..string.format("ROL=%.1f\n",ROL) + text=text..string.format("ROL=%.1f\n",ROL) text=text..G self:T3(self.lid..text) @@ -6943,24 +7192,55 @@ end --- Get short name of the grove step. -- @param #AIRBOSS self --- @param #number step Step +-- @param #number n Use -1 for previous or +1 for next. Default 0. +-- @param #boolean previous If true return previous step. E.g. -- @return #string Shortcut name "X", "RB", "IM", "AR", "IW". -function AIRBOSS:_GS(step) +function AIRBOSS:_GS(step, n) local gp + n=n or 0 + if step==AIRBOSS.PatternStep.FINAL then - gp="X0" -- Entering the groove. + gp=AIRBOSS.GroovePos.X0 --"X0" -- Entering the groove. + if n==-1 then + gp=AIRBOSS.GroovePos.X0 -- There is no previous step. + elseif n==1 then + gp=AIRBOSS.GroovePos.XX + end elseif step==AIRBOSS.PatternStep.GROOVE_XX then - gp="X" -- Starting the groove. - elseif step==AIRBOSS.PatternStep.GROOVE_RB then - gp="RB" -- Roger ball call. + gp=AIRBOSS.GroovePos.XX --"XX" -- Starting the groove. + if n==-1 then + gp=AIRBOSS.GroovePos.X0 + elseif n==1 then + gp=AIRBOSS.GroovePos.IM + end elseif step==AIRBOSS.PatternStep.GROOVE_IM then - gp="IM" -- In the middle. + gp=AIRBOSS.GroovePos.IM --"IM" -- In the middle. + if n==-1 then + gp=AIRBOSS.GroovePos.XX + elseif n==1 then + gp=AIRBOSS.GroovePos.IC + end elseif step==AIRBOSS.PatternStep.GROOVE_IC then - gp="IC" -- In close. + gp=AIRBOSS.GroovePos.IC --"IC" -- In close. + if n==-1 then + gp=AIRBOSS.GroovePos.IM + elseif n==1 then + gp=AIRBOSS.GroovePos.AR + end elseif step==AIRBOSS.PatternStep.GROOVE_AR then - gp="AR" -- At the ramp. + gp=AIRBOSS.GroovePos.AR --"AR" -- At the ramp. + if n==-1 then + gp=AIRBOSS.GroovePos.IC + elseif n==1 then + gp=AIRBOSS.GroovePos.IW + end elseif step==AIRBOSS.PatternStep.GROOVE_IW then - gp="IW" -- In the wires. + gp=AIRBOSS.GroovePos.IW --"IW" -- In the wires. + if n==-1 then + gp=AIRBOSS.GroovePos.AR + elseif n==1 then + gp=AIRBOSS.GroovePos.IW -- There is no next step. + end end return gp end @@ -7358,24 +7638,49 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. function AIRBOSS:_Debrief(playerData) - self:F2(self.lid..string.format("Debriefing of player %s.", playerData.name)) + self:F(self.lid..string.format("Debriefing of player %s.", playerData.name)) -- LSO grade, points, and flight data analyis. local grade, points, analysis=self:_LSOgrade(playerData) + + -- Insert points to table of all points until player landed. + table.insert(playerData.points, points) + + -- Player has landed and is not airborne any more. + local Points=0 + if playerData.landed and not playerData.unit:InAir() then + + -- Average over all points received so far. + for _,_points in pairs(playerData.points) do + Points=Points+_points + end - -- My LSO grade. + -- This is the final points. + Points=Points/#playerData.points + + -- Reset points array. + playerData.points={} + else + -- Player boltered or was waved off ==> We display the normal points. + Points=points + end + + -- My LSO grade. local mygrade={} --#AIRBOSS.LSOgrade mygrade.grade=grade mygrade.points=points mygrade.details=analysis mygrade.wire=playerData.wire mygrade.Tgroove=playerData.Tgroove + if playerData.landed and not playerData.unit:InAir() then + mygrade.finalscore=Points + end -- Add LSO grade to table. table.insert(playerData.grades, mygrade) -- LSO grade: (OK) 3.0 PT - LURIM - local text=string.format("%s %.1f PT - %s", grade, points, analysis) + local text=string.format("%s %.1f PT - %s", grade, Points, analysis) -- Wire and Groove time only if not pattern WO. if not playerData.patternwo then @@ -7452,7 +7757,7 @@ function AIRBOSS:_Debrief(playerData) end -- Re-enter message. - local text=string.format("fly heading %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) + local text=string.format("fly heading %03d° for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 5) else @@ -8066,7 +8371,7 @@ function AIRBOSS:RadioTransmit(radio, call, loud, delay) else -- Scheduled transmission. - SCHEDULER:New(self, self.RadioTransmission, {radio, call, loud}, delay) + SCHEDULER:New(nil, self.RadioTransmission, {self, radio, call, loud}, delay) end end @@ -8103,7 +8408,7 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration if delay and delay>0 then -- Delayed call. - SCHEDULER:New(self, self.MessageToPlayer, {playerData, message, sender, receiver, duration, clear, 0, soundoff}, delay) + SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear, 0, soundoff}, delay) else if sender and not soundoff then @@ -8250,7 +8555,7 @@ function AIRBOSS:_Number2Sound(playerData, sender, number, delay) if delay and delay>0 then -- Delayed call. - SCHEDULER:New(self, AIRBOSS._Number2Sound, {playerData, sender, number}, delay) + SCHEDULER:New(nil, AIRBOSS._Number2Sound, {self, playerData, sender, number}, delay) else -- Split string into characters. @@ -8410,10 +8715,9 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) -- F3 -- F10/Airboss//F1 Help/ missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F3 - missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._AttitudeMonitor, self, playername) -- F4 + missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._DisplayAttitude, self, _unitName) -- F4 missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) -- F5 missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) -- F6 - missionCommands.addCommandForGroup(gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) -- F7 ------------------------------------- -- F10/Airboss//F2 Kneeboard @@ -8426,16 +8730,18 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(gid, "My LSO Grades", _resultsPath, self._DisplayPlayerGrades, self, _unitName) -- F2 missionCommands.addCommandForGroup(gid, "Last Debrief", _resultsPath, self._DisplayDebriefing, self, _unitName) -- F3 -- F10/Airboss// ------------------------- - missionCommands.addCommandForGroup(gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) -- F3 - missionCommands.addCommandForGroup(gid, "Request Commence", _rootPath, self._RequestCommence, self, _unitName) -- F4 - missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) -- F5 + missionCommands.addCommandForGroup(gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "Request Commence", _rootPath, self._RequestCommence, self, _unitName) -- F4 + missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) -- F5 + missionCommands.addCommandForGroup(gid, "[Reset My Status]", _rootPath, self._ResetPlayerStatus, self, _unitName) -- F6 end else self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) @@ -8711,6 +9017,10 @@ function AIRBOSS:_RequestRefueling(_unitName) -- TODO: What if only the player and not his section wants to refuel?! self:_CollapseMarshalStack(playerData, true) end + + -- Set step to refueling. + playerData.step=AIRBOSS.PatternStep.REFUELING + elseif self.tanker:IsReturning() then -- Tanker is RTB. text="Tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." @@ -8817,33 +9127,54 @@ function AIRBOSS:_DisplayScoreBoard(_unitName) -- Results table. local _playerResults={} - -- Player data of requestor. - local playerData=self.players[_playername] --#AIRBOSS.PlayerData - - -- Message text. - local text = string.format("Greenie Board:") - + -- Calculate average points for all players. for _playerName,_playerData in pairs(self.players) do - + local playerData=_playerData --#AIRBOSS.PlayerData + local Paverage=0 - for _,_grade in pairs(_playerData.grades) do - Paverage=Paverage+_grade.points + local n=0 + for _,_grade in pairs(playerData.grades) do + local grade=_grade --#AIRBOSS.LSOgrade + + -- Add up points + Paverage=Paverage+grade.points + n=n+1 + end + if n>0 then + _playerResults[_playerName]=Paverage/n end - _playerResults[_playerName]=Paverage - end --Sort list! local _sort=function(a, b) return a>b end table.sort(_playerResults,_sort) - - local i=1 + + -- Message text. + local text = string.format("Greenie Board (top ten):") + local i=0 for _playerName,_points in pairs(_playerResults) do - text=text..string.format("\n[%d] %.1f %s", i,_points,_playerName) + text=text..string.format("\n[%d] %s %.1f||", i+1,_playerName,_points) + + -- Current player data. + local playerData=self.players[_playerName] --#AIRBOSS.PlayerData + + -- Add grades of passes. + for _,_grade in pairs(playerData.grades) do + local grade=_grade --#AIRBOSS.LSOgrade + text=text..string.format("|%.1f", grade.points) + end + i=i+1 + if i==10 then + break + end + end + if i==0 then + text=text.."\nNo results yet." end -- Send message. + local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData.client then MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) end @@ -8873,7 +9204,10 @@ function AIRBOSS:_DisplayPlayerGrades(_unitName) for i,_grade in pairs(playerData.grades) do local grade=_grade --#AIRBOSS.LSOgrade - text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, grade.points, grade.details) + -- Show final points or points of pass. + local points=grade.finalscore or grade.points + + text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, points, grade.details) -- Wire trapped if any. if grade.wire and grade.wire<=4 then @@ -8972,14 +9306,58 @@ end --- Turn player's aircraft attitude display on or off. -- @param #AIRBOSS self --- @param #string playername Player name. -function AIRBOSS:_AttitudeMonitor(playername) - self:F2({playername=playername}) +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayAttitude(_unitname) + self:F2(_unitname) - local playerData=self.players[playername] --#AIRBOSS.PlayerData + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) - if playerData then - playerData.attitudemonitor=not playerData.attitudemonitor + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.attitudemonitor=not playerData.attitudemonitor + end + end + +end + +--- Display marshal queue. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayMarshalQueue(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + + local text=string.format("Marshal Queue:") + if #self.Qmarshal==0 then + text=text.." empty" + else + for i,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + local charlie=self:_GetCharlieTime(flight) + local Charlie=UTILS.SecondsToClock(charlie) + local stack=flight.flag:Get() + text=text..string.format("\n[Stack %d] %s (%s): Case %d, Charlie %s", stack, flight.onboard, flight.actype, flight.case, tostring(Charlie)) + end + end + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", 10, true) + end end end @@ -9047,6 +9425,17 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) end end + -- Recovery tanker TACAN text. + local tankertext=nil + if self.tanker then + tankertext=string.format("Recovery tanker TACAN ") + if self.tanker.TACANon then + tankertext=tankertext..string.format("%d%s (%s)\n",self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) + else + tankertext=tankertext.."n/a\n" + end + end + -- Message text. local text=string.format("%s info:\n", self.alias) text=text..string.format("=============================================\n") @@ -9059,6 +9448,9 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) text=text..string.format("LSO radio %.3f MHz\n", self.LSOFreq) text=text..string.format("TACAN Channel %s\n", tacan) text=text..string.format("ICLS Channel %s\n", icls) + if tankertext then + text=text..tankertext + end text=text..string.format("# A/C total %d\n", #self.flights) text=text..string.format("# A/C marshal %d (%d)\n", Nmarshal, nmarshal) text=text..string.format("# A/C pattern %d (%d)\n", Npattern, npattern) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index cdec7ebad..f677f4c89 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -237,7 +237,7 @@ RECOVERYTANKER = { --- Class version. -- @field #string version -RECOVERYTANKER.version="1.0.0" +RECOVERYTANKER.version="1.0.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -739,7 +739,7 @@ function RECOVERYTANKER:onafterStart(From, Event, To) end -- Initialize route. self.distStern<0! - SCHEDULER:New(self, self._InitRoute, {-self.distStern+UTILS.NMToMeters(3)}, 1) + SCHEDULER:New(nil, self._InitRoute, {self, -self.distStern+UTILS.NMToMeters(3)}, 1) --self:_InitRoute(-self.distStern+UTILS.NMToMeters(3), 1) -- Create tanker beacon. @@ -975,7 +975,7 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) end -- Initial route. - SCHEDULER:New(self, self._InitRoute, {-self.distStern+UTILS.NMToMeters(3)}, 2) + SCHEDULER:New(nil, self._InitRoute, {self, -self.distStern+UTILS.NMToMeters(3)}, 2) end @@ -1209,7 +1209,7 @@ function RECOVERYTANKER:_ActivateTACAN(delay) if delay and delay>0 then -- Schedule TACAN activation. - SCHEDULER:New(self, self._ActivateTACAN, {}, delay) + SCHEDULER:New(nil, self._ActivateTACAN, {self}, delay) else diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 7a39d29d7..9a84fef64 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -218,7 +218,7 @@ RESCUEHELO = { --- Class version. -- @field #string version -RESCUEHELO.version="1.0.0" +RESCUEHELO.version="1.0.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -530,10 +530,10 @@ end --- Set offset perpendicular to orientation to carrier. -- @param #RESCUEHELO self --- @param #number distance Offset distance in meters. Default 100 m. +-- @param #number distance Offset distance in meters. Default 240 m. -- @return #RESCUEHELO self function RESCUEHELO:SetOffsetZ(distance) - self.offsetZ=distance or 100 + self.offsetZ=distance or 240 return self end From 9700776468060b7ba5f57fbdbb90edd517bf031e Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 3 Jan 2019 14:09:19 +0100 Subject: [PATCH 126/147] Controllable log output Controllable, changed self:E to self:F/T2 since it was spamming the DCS log. Airboss docs. Spawn scheduler(self,...) --> scheduler(nil,...) --- Moose Development/Moose/Core/Spawn.lua | 2 +- Moose Development/Moose/Ops/Airboss.lua | 15 ++++++++------- Moose Development/Moose/Wrapper/Controllable.lua | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 0d4156260..63ca1b401 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2639,7 +2639,7 @@ function SPAWN:_OnEngineShutDown( EventData ) self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) --self:ReSpawn( SpawnGroupIndex ) -- Delay respawn by three seconds due to DCS 2.5.4 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 - SCHEDULER:New(self, self.ReSpawn, {SpawnGroupIndex}, 3) + SCHEDULER:New(nil, self.ReSpawn, {self, SpawnGroupIndex}, 3) end end end diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index cd5d21a5c..9cb74aaf5 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -278,6 +278,7 @@ -- * **F3 Request Marshal** -- * **F4 Request Commence** -- * **F5 Request Refueling** +-- * **F6 [Reset My Status]** -- -- ### Request Marshal -- @@ -380,12 +381,12 @@ -- -- ## LSO Grading -- --- LSO grading starts when the player enters the groove. The flight path and aircraft attitude is evaluated at certain steps +-- LSO grading starts when the player enters the groove. The flight path and aircraft attitude is evaluated at certain steps (distances measured from rundown): -- --- * **X** At the Start (0.75 NM = 1389 m from the rundown). --- * **IM** In the Middle (0.375 NM = 695 from the rundown). --- * **IC** In Close (0.18 NM = 333 m from the rundown). --- * **AR** At the Ramp (0.027 NM = 50 m from the rundown). +-- * **X** At the Start (0.75 NM = 1390 m). +-- * **IM** In the Middle (0.375 NM = 695 m). +-- * **IC** In Close (0.18 NM = 333 m). +-- * **AR** At the Ramp (0.027 NM = 50 m). -- * **IW** In the Wiress (at the landing position). -- -- Grading at each step includes the above calls, i.e. @@ -393,7 +394,7 @@ -- * **L**ined **U**p **L**eft or **R**ight: LUL, LUR -- * Too **H**igh or too **LO**w: H, LO -- * Too **F**ast or too **SLO**w: F, SLO --- * Fly through **down* or **up**: \\, / +-- * **Fly through** glide slope **down** or **up**: \\ , / -- -- Each grading, x, is subdivided by -- @@ -408,7 +409,7 @@ -- * Glide slope error < -1.2 degrees or > 1.8 degrees and/or -- * AOA depending on aircraft type and only applied if skill level is "TOPGUN graduate". -- --- Line up and glide slope error thresholds were tested using [https://forums.eagle.ru/showthread.php?t=211557](VFA-113 Stingers LSO Mod), +-- Line up and glide slope error thresholds were tested using [VFA-113 Stingers LSO Mod](https://forums.eagle.ru/showthread.php?t=211557), -- if the aircraft is outside the red box. -- -- ## Pattern Wave Off diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index d037d734f..0353c42f9 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -381,7 +381,7 @@ end -- @param #number WaitTime Time in seconds, before the task is set. -- @return Wrapper.Controllable#CONTROLLABLE self function CONTROLLABLE:SetTask( DCSTask, WaitTime ) - self:E( { "SetTask", WaitTime, DCSTask = DCSTask } ) + self:F( { "SetTask", WaitTime, DCSTask = DCSTask } ) local DCSControllable = self:GetDCSObject() @@ -389,7 +389,7 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) local DCSControllableName = self:GetName() - self:E( "Controllable Name = " .. DCSControllableName ) + self:T2( "Controllable Name = " .. DCSControllableName ) -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. -- Therefore we schedule the functions to set the mission and options for the Controllable. From 09f28a555f887fdde3900d87c5b738314dae1194 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 4 Jan 2019 00:22:42 +0100 Subject: [PATCH 127/147] AIRBOSS v0.7.1 * Fixed radio transmission subtitle bug. * Added on-the-fly LSO grading to attitude monitor. * Added doc menu images. --- Moose Development/Moose/Ops/Airboss.lua | 99 +++++++++++++++++++------ 1 file changed, 75 insertions(+), 24 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 9cb74aaf5..2d97deefa 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -269,7 +269,9 @@ -- The F10 radio menu can be used to post requests to Marshal but also provides information about the player and carrier status. Additionally, helper functions -- can be called. -- --- ## Main Menu +-- ## Root Menu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuRoot.png) -- -- The general structure -- @@ -306,10 +308,14 @@ -- -- ## Help Menu -- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuHelp.png) +-- -- This menu provides commands to help the player. -- -- ### Mark Zones Submenu -- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMarkZones.png) +-- -- These commands can be used to mark marshal or landing pattern zones. -- -- * **Smoke Pattern Zones** Smoke is used to mark the landing pattern zone of the player depending on his recovery case. @@ -317,9 +323,15 @@ -- * **Flare Pattern Zones** Similar to smoke but uses flares to mark the pattern zones. -- * **Smoke Marshal Zone** This smokes the surrounding area of the currently assigned Marshal zone of the player. Player has to be registered in Marshal queue. -- * **Flare Marshal Zone** Similar to smoke but uses flares to mark the Marshal zone. +-- +-- Note that the smoke lasts ~5 minutes but the zones are moving along with the carrier. So after some time, the smoke gives shows you a picture of the past. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3_FlarePattern.png) -- -- ### Skill Level Submenu -- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuSkill.png) +-- -- The player can choose between three skill or difficulty levels. -- -- * **Flight Student**: The player receives tips at certain stages of the pattern, e.g. if he is at the right altitude, speed, etc. @@ -328,13 +340,19 @@ -- -- ### My Status -- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMyStatus.png) +-- -- This command provides information about the current player status. For example, his current step in the pattern. -- -- ### Attitude Monitor -- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuAttitudeMonitor.png) +-- -- This command displays the current aircraft attitude of the player in short intervals as message on the screen. -- It provides information about current pitch, roll, yaw, lineup and glideslope error, orientation of the plane wrt to carrier etc. -- +-- If you are in the groove, current lineup and glide slope errors are displayed and you get an on-the-fly LSO grade. +-- -- ### LSO Radio Check -- -- LSO will transmit a short message on his radio frequency. See @{#AIRBOSS.SetLSORadio}. @@ -345,10 +363,14 @@ -- -- ## Kneeboard Menu -- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuKneeboard.png) +-- -- The Kneeboard menu provides information about the carrier, weather and player results. -- -- ### Results Submenu -- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuResults.png) +-- -- Here you find your LSO grading results as well as scores of other players. -- -- * **Greenie Board** lists average scores of all players obtained during landing approaches. @@ -357,10 +379,14 @@ -- -- ### Carrier Info -- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuCarrierInfo.png) +-- -- Information about the current carrier status is displayed. This includes current BRC, FB, LSO and Marshal frequences, list of next recovery windows. -- -- ### Weather Report -- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuWeatherReport.png) +-- -- Displays information about the current weather at the carrier such as QFE, wind and temperature. -- -- ### Set Section @@ -368,6 +394,12 @@ -- With this command, you can define a section of human flights. The player how issues the command becomes the section lead and all other human players -- within a radius of 200 meters become members of the section. -- +-- ### Marshal Queue +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMarshalQueue.png) +-- +-- Lists all flights currently in the Marshal queue including their assigned stack, recovery case and Charie time estimate. +-- -- # Landing Signal Officer (LSO) -- -- The LSO will first contact you on his radio channel when you are at the the abeam position (Case I) with the phrase "Paddles, contact.". @@ -408,9 +440,17 @@ -- * Line up error > 3.0 degrees left or right and/or -- * Glide slope error < -1.2 degrees or > 1.8 degrees and/or -- * AOA depending on aircraft type and only applied if skill level is "TOPGUN graduate". --- --- Line up and glide slope error thresholds were tested using [VFA-113 Stingers LSO Mod](https://forums.eagle.ru/showthread.php?t=211557), --- if the aircraft is outside the red box. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_LSOPlatcam.png) +-- +-- Line up and glide slope error thresholds were tested extensively using [VFA-113 Stingers LSO Mod](https://forums.eagle.ru/showthread.php?t=211557), +-- if the aircraft is outside the red box. In the picture above, **blue** numbers denote the line up thresholds while the **blacks** refer to the glide slope. +-- +-- A wave off is called, when the aircraft is outside the red rectangle. The measurement stops already 50 m before the rundown, since the error in the calculation +-- increases the closer the aircraft gets to the origin/reference point. +-- +-- The optimal glide slope is assumed to be 3.5 degrees leading to a touch down point between the second and third wire. +-- The height of the carrier deck and the exact wire locations are taken into account in the calculations. -- -- ## Pattern Wave Off -- @@ -1141,7 +1181,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.7.0" +AIRBOSS.version="0.7.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1317,7 +1357,7 @@ function AIRBOSS:New(carriername, alias) self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) + BASE:TraceLevel(3) end -- Smoke zones. @@ -3978,6 +4018,7 @@ function AIRBOSS:_InitPlayer(playerData, step) -- Set us up on final if group name contains "Groove". But only for the first pass. if playerData.group:GetName():match("Groove") and playerData.passes==0 then self:MessageToPlayer(playerData, "Group name contains \"Groove\". Happy groove testing.") + playerData.attitudemonitor=true playerData.step=AIRBOSS.PatternStep.FINAL end @@ -4608,6 +4649,9 @@ function AIRBOSS:OnEventLand(EventData) -- Unkonwn step until we now more. playerData.step=AIRBOSS.PatternStep.UNDEFINED + + -- Switch attitude monitor off if on. + playerData.attitudemonitor=false -- Call trapped function in 1 second to make sure we did not bolter. SCHEDULER:New(nil, self._Trapped, {self, playerData}, 1) @@ -6465,10 +6509,10 @@ function AIRBOSS:_AttitudeMonitor(playerData) -- Output local text=string.format("Pattern step: %s\n", playerData.step) text=text..string.format("AoA=%.1f | |V|=%.1f knots\n", aoa, UTILS.MpsToKnots(vabs)) - text=text..string.format("Vx=%.1f Vy=%.1f Vz=%.1f m/s\n", velo.x, velo.y, velo.z) + text=text..string.format("Vx=%.1f Vy=%.1f Vz=%.1f m/s\n", velo.x, velo.y, velo.z) text=text..string.format("Pitch=%.1f° | Roll=%.1f° | Yaw=%.1f°\n", pitch, roll, yaw) text=text..string.format("Climb Angle=%.1f° | Rate=%d ft/min\n", unit:GetClimbAngle(), velo.y*196.85) - text=text..string.format("R=%.1f NM | X=%d Z=%d m\n", UTILS.MetersToNM(rho), dx, dz) + text=text..string.format("R=%.2f NM | X=%d Z=%d m\n", UTILS.MetersToNM(rho), dx, dz) text=text..string.format("Gamma=%.1f°", relhead) -- If in the groove, provide line up and glide slope error. if playerData.step==AIRBOSS.PatternStep.GROOVE_X0 or @@ -6479,14 +6523,16 @@ function AIRBOSS:_AttitudeMonitor(playerData) playerData.step==AIRBOSS.PatternStep.GROOVE_IW then local lineup=self:_Lineup(playerData.unit, true) local glideslope=self:_Glideslope(playerData.unit, 3.5) - text=text..string.format("\nLU Error = %.1f° (line up)", lineup) - text=text..string.format("\nGS Error = %.1f° (glide slope)", glideslope) + text=text..string.format("\nLineUp=%.2f° | GlideSlope=%.2f°", lineup, glideslope) + --text=text..string.format("\nGlideslope = ", glideslope) + local grade, points, analysis=self:_LSOgrade(playerData) + text=text..string.format("\n%s %.1f PT - %s", grade, points, analysis) end -- Wind (for debugging). --text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) - MESSAGE:New(text, 3, nil , true):ToClient(playerData.client) + MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) end --- Get glide slope of aircraft unit. @@ -7423,29 +7469,31 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) -- Altitude error +-X% local _error=(altitude-altopt)/altopt*100 - local radiocall={} --#AIRBOSS.RadioCall + -- TODO: radio call for flight students. + --local radiocall={} --#AIRBOSS.RadioCall local hint if _error>badscore then hint=string.format("You're high.") - radiocall=AIRBOSS.LSOCall.HIGH - radiocall.loud=true - radiocall.subtitle="" + -- TODO: Dang! This overwrites the array! + --radiocall=AIRBOSS.LSOCall.HIGH + --radiocall.loud=true + --radiocall.subtitle="" elseif _error>lowscore then hint= string.format("You're slightly high.") - radiocall=AIRBOSS.LSOCall.HIGH - radiocall.loud=false - radiocall.subtitle="" + --radiocall=AIRBOSS.LSOCall.HIGH + --radiocall.loud=false + --radiocall.subtitle="" elseif _error<-badscore then hint=string.format("You're low. ") - radiocall=AIRBOSS.LSOCall.LOW - radiocall.loud=true - radiocall.subtitle="" + --radiocall=AIRBOSS.LSOCall.LOW + --radiocall.loud=true + --radiocall.subtitle="" elseif _error<-lowscore then hint=string.format("You're slightly low.") - radiocall=AIRBOSS.LSOCall.LOW - radiocall.loud=false - radiocall.subtitle="" + --radiocall=AIRBOSS.LSOCall.LOW + --radiocall.loud=false + --radiocall.subtitle="" else hint=string.format("Good altitude.") end @@ -7640,6 +7688,9 @@ end -- @param #AIRBOSS.PlayerData playerData Player data. function AIRBOSS:_Debrief(playerData) self:F(self.lid..string.format("Debriefing of player %s.", playerData.name)) + + -- Switch attitude monitor off if on. + playerData.attitudemonitor=false -- LSO grade, points, and flight data analyis. local grade, points, analysis=self:_LSOgrade(playerData) From acd6e0f42353e298f21210ff94f59d595aa81489 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 5 Jan 2019 09:51:19 +0100 Subject: [PATCH 128/147] AIRBOSS v0.7.2 --- Moose Development/Moose/Ops/Airboss.lua | 188 ++++++++++++++++++------ 1 file changed, 139 insertions(+), 49 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 2d97deefa..9c03012fb 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -93,7 +93,6 @@ -- @field Core.Scheduler#SCHEDULER radiotimer Radio queue scheduler. -- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. -- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. --- @field Core.Zone#ZONE_UNIT zoneInitial Zone usually 3 NM astern of carrier where pilots start their CASE I pattern. -- @field #table players Table of players. -- @field #table menuadded Table of units where the F10 radio menu was added. -- @field #AIRBOSS.Checkpoint BreakEntry Break entry checkpoint. @@ -129,6 +128,7 @@ -- @field #number magvar Magnetic declination in degrees. -- @field #number Tcollapse Last time timer.gettime() the stack collapsed. -- @field #AIRBOSS.Recovery recoverywindow Current or next recovery window opened. +-- @field #boolean usersoundradio Use user sound output instead of radio transmissions. -- @extends Core.Fsm#FSM --- Be the boss! @@ -559,7 +559,6 @@ AIRBOSS = { radiotimer = nil, zoneCCA = nil, zoneCCZ = nil, - zoneInitial = nil, players = {}, menuadded = {}, BreakEntry = {}, @@ -595,6 +594,7 @@ AIRBOSS = { magvar = nil, Tcollapse = nil, recoverywindow = nil, + usersoundradio = nil, } --- Player aircraft types capable of landing on carriers. @@ -1181,7 +1181,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.7.1" +AIRBOSS.version="0.7.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1346,7 +1346,7 @@ function AIRBOSS:New(carriername, alias) end -- CASE I/II moving zone: Zone 2.75 NM astern and 0.1 NM starboard of the carrier with a diameter of 1 NM. - self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, UTILS.NMToMeters(0.5), {dx=-UTILS.NMToMeters(2.75), dy=UTILS.NMToMeters(0.1), relative_to_unit=true}) + --self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, UTILS.NMToMeters(0.5), {dx=-UTILS.NMToMeters(2.75), dy=UTILS.NMToMeters(0.1), relative_to_unit=true}) ------------------- -- Debug Section -- @@ -1767,6 +1767,13 @@ function AIRBOSS:SetMarshalRadio(frequency, modulation) return self end +--- Use user sound output instead of radio transmission for messages. Might be handy if radio transmissions are broken. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetUserSoundRadio() + self.usersoundradio=true +end + --- Set number of aircraft units which can be in the landing pattern before the pattern is full. -- @param #AIRBOSS self -- @param #number nmax Max number. Default 4. @@ -2644,8 +2651,8 @@ function AIRBOSS:_InitStennis() self.BreakEntry.name="Break Entry" self.BreakEntry.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. self.BreakEntry.Xmax= nil - self.BreakEntry.Zmin=-400 -- Not more than 400 m port of boat. Otherwise miss the zone. - self.BreakEntry.Zmax=UTILS.NMToMeters(1.5) -- Not more than 1.5 NM starboard. + self.BreakEntry.Zmin=-UTILS.NMToMeters(0.5) -- Not more than 0.5 NM port of boat. + self.BreakEntry.Zmax= UTILS.NMToMeters(1.5) -- Not more than 1.5 NM starboard. self.BreakEntry.LimitXmin=0 -- Check and next step when at carrier and starboard of carrier. self.BreakEntry.LimitXmax=nil self.BreakEntry.LimitZmin=nil @@ -4945,7 +4952,7 @@ end function AIRBOSS:_Initial(playerData) -- Check if player is in initial zone and entering the CASE I pattern. - local inzone=playerData.unit:IsInZone(self.zoneInitial) + local inzone=playerData.unit:IsInZone(self:_GetZoneInitial(playerData.case)) -- Relative heading to carrier direction. local relheading=self:_GetRelativeHeading(playerData.unit, false) @@ -6121,6 +6128,56 @@ end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ZONE functions ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get Initial zone for Case I or II. +-- @param #AIRBOSS self +-- @param #number case Recovery Case. +-- @return Core.Zone#ZONE_POLYGON_BASE Initial zone. +function AIRBOSS:_GetZoneInitial(case) + + -- Get radial, i.e. inverse of BRC. + local radial=self:GetRadial(2, false, false) + + -- Carrier coordinate. + local cv=self:GetCoordinate() + + -- Zone and vec2 array. + local zone + local vec2 + + if case==1 then + -- Case I + + local c1=cv:Translate(UTILS.NMToMeters(0.5), radial-90) -- 0.0 0.5 starboard + local c2=c1:Translate(UTILS.NMToMeters(3), radial):Translate(1, radial-90) -- -3.0 1.5 starboard, astern + local c3=c2:Translate(UTILS.NMToMeters(2), radial+90) + local c4=cv:Translate(UTILS.NMToMeters(0.5), radial) + local c5=cv + + -- Vec2 array. + vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2()} + + else + -- Case II + + -- Funnel. + local c1=cv:Translate(UTILS.NMToMeters(0.5), radial-90) -- 0.0, 0.5 + local c2=c1:Translate(UTILS.NMToMeters(0.5), radial) -- 0.5, 0.5 + local c3=cv:Translate(UTILS.NMToMeters(1.2), radial-90):Translate(UTILS.NMToMeters(3), radial) -- 3.0, 1.2 + local c4=cv:Translate(UTILS.NMToMeters(1.2), radial+90):Translate(UTILS.NMToMeters(3), radial) -- 3.0,-1.2 + local c5=cv:Translate(UTILS.NMToMeters(0.5), radial) + local c6=cv + + -- Vec2 array. + vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2()} + + end + + -- Polygon zone. + local zone=ZONE_POLYGON_BASE:New("CASE I/II initial.", vec2) + + return zone +end + --- Get Bullseye zone with radius 1 NM and DME 3 NM from the carrier. Radial depends on recovery case. -- @param #AIRBOSS self @@ -6150,7 +6207,7 @@ end --- Get dirty up zone with radius 1 NM and DME 9 NM from the carrier. Radial depends on recovery case. -- @param #AIRBOSS self -- @param #number case Recovery case. --- @return Core.Zone#ZONE_RADIUS Arc in zone. +-- @return Core.Zone#ZONE_RADIUS Dirty up zone. function AIRBOSS:_GetZoneDirtyUp(case) -- Radius = 1 NM. @@ -6179,10 +6236,10 @@ end function AIRBOSS:_GetZoneArcOut(case) -- Radius = 1 NM. - local radius=UTILS.NMToMeters(1) + local radius=UTILS.NMToMeters(1.25) -- Distance = 12 NM - local distance=UTILS.NMToMeters(12) + local distance=UTILS.NMToMeters(11.75) -- Zone depends on Case recovery. local radial=self:GetRadial(case, false, false) @@ -6203,7 +6260,7 @@ end function AIRBOSS:_GetZoneArcIn(case) -- Radius = 1 NM. - local radius=UTILS.NMToMeters(1) + local radius=UTILS.NMToMeters(1.25) -- Zone depends on Case recovery. local radial=self:GetRadial(case, false, true) @@ -6212,7 +6269,7 @@ function AIRBOSS:_GetZoneArcIn(case) local alpha=math.rad(self.holdingoffset) -- 12+x NM from carrier - local x=12/math.cos(alpha) + local x=14/math.cos(alpha) -- Distance = 14 NM local distance=UTILS.NMToMeters(x) @@ -6280,7 +6337,6 @@ function AIRBOSS:_GetZoneCorridor(case) -- Length of the box in NM. local x=(d+w/2)/math.cos(alpha) local l=28-x - --local l=15 --/math.cos(alpha) -- Some math... local y1=d-w2 @@ -6320,6 +6376,10 @@ function AIRBOSS:_GetZoneCorridor(case) c[9]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) -- 1 left of carrier. c[8]=c[9]:Translate( UTILS.NMToMeters(d+dx-w2), radial) -- 1 left and 11 behind of carrier. c[7]=c[8]:Translate( UTILS.NMToMeters(P), radial+90) + + -- Translate these points a bit for a smoother turn. + c[4]=c[4]:Translate(UTILS.NMToMeters(2), offset) + c[7]=c[7]:Translate(UTILS.NMToMeters(2), offset) else -- Easy case of a long box. c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) @@ -7789,10 +7849,13 @@ function AIRBOSS:_Debrief(playerData) -- Next step: Initial again. playerData.step=AIRBOSS.PatternStep.INITIAL + + -- Create a point 3.0 NM astern for re-entry. + local zoneinitial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5), self:GetRadial(2, false, false, false)) -- Get heading and distance to initial zone ~3 NM astern. - heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) - distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) + heading=playerData.unit:GetCoordinate():HeadingTo(zoneinitial) + distance=playerData.unit:GetCoordinate():Get2DDistance(zoneinitial) elseif playerData.case==3 then @@ -7841,14 +7904,20 @@ function AIRBOSS:_Debrief(playerData) end else + + -- Welcome aboard! + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) -- Airboss talkto! local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") - self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 2) + self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 3) + -- Remove player unit from flight and all queues. + self:_RemoveUnitFromFlight(playerData.unit) + -- Next step undefined. Player landed. playerData.step=AIRBOSS.PatternStep.UNDEFINED - + end elseif playerData.boltered then @@ -8402,18 +8471,31 @@ function AIRBOSS:RadioTransmit(radio, call, loud, delay) end end filename=filename.."."..(call.suffix or "ogg") + + -- Output via radio transmision or user sound. + if self.usersoundradio then + + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + if playerData.unit:IsInZone(self.zoneCCA) then + USERSOUND:New(filename):ToGroup(playerData.group) + end + end + + else - -- New transmission. - radio:NewUnitTransmission(filename, call.subtitle, call.duration, radio.Frequency/1000000, radio.Modulation, false) + -- New transmission. + radio:NewUnitTransmission(filename, call.subtitle, call.duration, radio.Frequency/1000000, radio.Modulation, false) + + -- Broadcast message. + radio:Broadcast(true) - -- Broadcast message. - radio:Broadcast(true) - - -- Workaround for the community A-4E-C as long as their radios are not functioning properly. - for _,_player in pairs(self.players) do - local playerData=_player --#AIRBOSS.PlayerData - if playerData.actype==AIRBOSS.AircraftCarrier.A4EC then - USERSOUND:New(filename):ToGroup(playerData.group) + -- Workaround for the community A-4E-C as long as their radios are not functioning properly. + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + if playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + USERSOUND:New(filename):ToGroup(playerData.group) + end end end @@ -8423,7 +8505,7 @@ function AIRBOSS:RadioTransmit(radio, call, loud, delay) else -- Scheduled transmission. - SCHEDULER:New(nil, self.RadioTransmission, {self, radio, call, loud}, delay) + SCHEDULER:New(nil, self.RadioTransmit, {self, radio, call, loud}, delay) end end @@ -9622,23 +9704,31 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) if playerData.step==AIRBOSS.PatternStep.INITIAL then + -- Create a point 3.0 NM astern for re-entry. + local zoneinitial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5), self:GetRadial(2, false, false, false)) + -- Heading and distance to initial zone. - local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) - local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate())) + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(zoneinitial) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(zoneinitial)) local brc=self:GetBRC() -- Help player to find its way to the initial zone. text=text..string.format("\nFly heading %03d° for %.1f NM and turn to BRC %03d°.", flyhdg, flydist, brc) elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then + + -- Coordinate of the platform zone. + local zoneplatform=self:_GetZonePlatform(playerData.case):GetCoordinate() -- Heading and distance to platform zone. - local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self:_GetZonePlatform(playerData.case):GetCoordinate()) - local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate())) - local fb=self:GetFinalBearing(true) + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(zoneplatform) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(zoneplatform)) + + -- Get heading. + local hdg=self:GetRadial(playerData.case, true, true, true) -- Help player to find its way to the initial zone. - text=text..string.format("\nFly heading %03d° for %.1f NM and turn to FB %03d°.", flyhdg, flydist, fb) + text=text..string.format("\nFly heading %03d° for %.1f NM and turn to FB %03d°.", flyhdg, flydist, hdg) end @@ -9684,7 +9774,7 @@ function AIRBOSS:_MarkMarshalZone(_unitName, flare) zone:FlareZone(FLARECOLOR.White, 45, nil, patternalt) else text="Marking marshal zone with WHITE smoke." - zone:SmokeZone(SMOKECOLOR.White, 45, patternalt) + zone:SmokeZone(SMOKECOLOR.Blue, 45, patternalt) end else @@ -9725,14 +9815,14 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) -- Case I/II: Initial if case==1 or case==2 then - text=text.."* initial with WHITE flares\n" - self.zoneInitial:FlareZone(FLARECOLOR.White, 45) + text=text.."* initial with GREEN flares\n" + self:_GetZoneInitial(case):FlareZone(FLARECOLOR.Green, 45) end -- Case II/III: approach corridor if case==2 or case==3 then - text=text.."* approach corridor with GREEN flares\n" - self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) + text=text.."* approach corridor with YELLOW flares\n" + self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Yellow, 45) end -- Case II/III: platform @@ -9750,8 +9840,8 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) -- Case II/III: arc in/out if case==2 or case==3 then if math.abs(self.holdingoffset)>0 then - self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.Yellow, 45) - text=text.."* arc turn in with YELLOW flares\n" + self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.White, 45) + text=text.."* arc turn in with WHITE flares\n" self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White, 45) text=text.."* arc trun out with WHITE flares\n" end @@ -9759,22 +9849,22 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) -- Case III: bullseye if case==3 then - text=text.."* bullseye with WHITE flares\n" - self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.White, 45) + text=text.."* bullseye with GREEN flares\n" + self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.Green, 45) end else -- Case I/II: Initial if case==1 or case==2 then - text=text.."* initial with WHITE smoke\n" - self.zoneInitial:SmokeZone(SMOKECOLOR.White, 45) + text=text.."* initial with GREEN smoke\n" + self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Green, 45) end -- Case II/III: Approach Corridor if case==2 or case==3 then - text=text.."* approach corridor with GREEN smoke\n" - self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + text=text.."* approach corridor with ORANGE smoke\n" + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Orange, 45) end -- Case II/III: platform @@ -9801,8 +9891,8 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) -- Case III: bullseye if case==3 then - text=text.."* bullseye with WHITE smoke\n" - self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) + text=text.."* bullseye with GREEN smoke\n" + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Green, 45) end end From 6edcc58b9ad0f675a5f0a942a5578eb2ea3fa066 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 9 Jan 2019 07:36:36 +0100 Subject: [PATCH 129/147] updates --- .../Moose/AI/AI_A2G_Dispatcher.lua | 87 +++++++++++-------- .../Moose/Functional/Detection.lua | 2 +- 2 files changed, 54 insertions(+), 35 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index facc45a66..35729f0b3 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -46,13 +46,14 @@ -- ## 1. Which coalition am I modeling an A2G defense system for? blue or red? -- -- One AI_A2G_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. --- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI_A2G_DISPATCHER **objects**, +-- If you want to create a **mutual defense system**, for both blue and red, then you need to instantiate **two** AI_A2G_DISPATCHER **objects**, -- each governing their defense system for one coalition. -- -- -- ## 2. Which type of detection will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). -- --- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units and reporting them to the head quarters. +-- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units +-- and reporting them to the head quarters. -- Several types of @{Functional.Detection} classes exist, and the most common characteristics of these classes is that they: -- -- * Perform detections from multiple recce as one co-operating entity. @@ -61,62 +62,82 @@ -- * Communicates detections. -- -- --- ## 3. Which recce units can be used as part of the detection system? Only Ground or also Airborne? +-- ## 3. Which recce units can be used as part of the detection system? Only ground based, or also airborne? -- --- Depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets. --- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range. --- Recce are very useful to acquire the position of enemy ground targets when spread out over the battlefield at strategic positions. --- Ground units also have varying detectors, and especially the ground units which have laser guiding missiles can be extremely effective at +-- Depending on the type of mission you want to achieve, different types of units can be engaged to perform ground enemy targets reconnaissance. +-- Ground recce (FAC) are very useful units to determine the position of enemy ground targets when they spread out over the battlefield at strategic positions. +-- Using their varying detection technology, and especially those ground units which have spotting technology, can be extremely effective at -- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. +-- Unfortunately, they lack sometimes the visibility to detect targets at greater range, or when scenery is preventing line of sight. -- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then -- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! -- --- Beside ground level units to use for reconnaissance, air units are also very effective. The are capable of patrolling at great speed --- covering a large terrain. However, airborne recce can be vulnerable to air to ground attacks, and you need air superiority to make then --- effective. Also the instruments available at the air units play a big role in the effectiveness of the reconnaissance. --- Air units which have ground detection capabilities will be much more effective than air units with only visual detection capabilities. --- For the red coalition, the Mi-28N and for the blue side, the reaper are such effective reconnaissance airborne units. +-- Airborne recce (AFAC) are also very effective. The are capable of patrolling at a functional detection altitude, +-- having an overview of the whole battlefield. However, airborne recce can be vulnerable to air to ground attacks, +-- so you need air superiority to make them effective. +-- Airborne recce will also have varying ground detection technology, which plays a big role in the effectiveness of the reconnaissance. +-- Certain helicopter or plane types have ground searching radars or advanced ground scanning technology, and are very effective +-- compared to air units having only visual detection capabilities. +-- For example, for the red coalition, the Mi-28N and the Su-34; and for the blue side, the reaper, are such effective airborne recce units. +-- +-- Typically, don't want these recce units to engage with the enemy, you want to keep them at position. Therefore, it is a good practice +-- to set the ROE for these recce to hold weapons, and make them invisible from the enemy. +-- +-- It is not possible to perform a recce function as a player (unit). -- -- --- ## 4. How do defenses decide to engage on approaching enemy units? +-- ## 4. How do the defenses decide **when and where to engage** on approaching enemy units? -- --- The A2G dispacher needs you to setup defense coordinates, which are specific coordinates that are strategic positions in the battle field --- to be defended. Any ground based enemy approaching to such a defense point, will be engaged for defense by A2G defense units. --- The A2G dispatcher provides parameters to setup the defensiveness, meaning, when actually A2G units will engage with the approaching enemy. --- For this, a probability distribution model has been created, which models an increased probability that a defense will engage an attacker, --- depending on the distance of the attacker to the defense coordinate. There are 3 levels of defense reactivity setup, which are Low, Medium and High. --- Defenses will start to consider defensive action when an enemy ground unit is within 60km from a defense point, by default. --- But you can change this maximum distance using on of the available methods. The close the attacker is to the defense point, the --- higher the probability will be that a defense action will be launched! +-- The A2G dispacher needs you to setup (various) defense coordinates, which are strategic positions in the battle field to be defended. +-- Any ground based enemy approaching within the proximity of such a defense point, may trigger for a defensive action by friendly air units. +-- +-- There are 2 important parameters that play a role in the defensive decision making: defensiveness and reactivity. +-- +-- The A2G dispatcher provides various parameters to setup the **defensiveness**, +-- which models the decision **when** a defender will engage with the approaching enemy. +-- Defensiveness is calculated by a probability distribution model when to trigger a defense action, +-- depending on the distance of the enemy unit from the defense coordinates, and a **defensiveness factor**. +-- +-- The other parameter considered for defensive action is **where the enemy is located**, thus the distance from a defense coordinate, +-- which we call the **reactive distance**. By default, the reactive distance is set to 60km, but can be changed by the mission designer +-- using the available method explained further below. +-- The combination of the defensiveness and reactivity results in a model that, the closer the attacker is to the defense point, +-- the higher the probability will be that a defense action will be launched! -- -- -- ## 5. Are defense coordinates and defense reactivity the only parameters? -- -- No, depending on the target type, and the threat level of the target, the probability of defense will be higher. -- In other words, when a SAM-10 radar emitter is detected, its probabilty for defense will be much higher than when a BMP-1 vehicle is --- detected, even when both are at the same distance from a defense coordinate. --- This will ensure optimal defenses, SEAD tasks will be much more quicker launched agains radar emitters, to ensure air superiority. --- Approaching main battle tanks will be much faster defended upon, than a group of approaching trucks. +-- detected, even when both enemies are at the same distance from a defense coordinate. +-- This will ensure optimal defenses, SEAD tasks will be launched much more quicker against engaging radar emitters, to ensure air superiority. +-- Approaching main battle tanks will be engaged much faster, than a group of approaching trucks. -- -- -- ## 6. Which Squadrons will I create and which name will I give each Squadron? -- -- The A2G defense system works with **Squadrons**. Each Squadron must be given a unique name, that forms the **key** to the squadron. --- Several options and activities can be set per Squadron. +-- Several options and activities can be set per Squadron. A free format name can be given, but always ensure that the name is meaningfull +-- for your mission, and remember that squadron names are used for communication to the players of your mission. -- --- There are mainly 3 types of defenses: SEAD, CAS and BAI. +-- There are mainly 3 types of defenses: **SEAD**, **CAS** and **BAI**. -- -- Suppression of Air Defenses (SEAD) are effective agains radar emitters. Close Air Support (CAS) is launched when the enemy is close near friendly units. -- Battleground Air Interdiction (BAI) tasks are launched when there are no friendlies around. -- -- Depending on the defense type, different payloads will be needed. See further points on squadron definition. -- +-- -- ## 7. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? -- --- Squadrons are placed as the "home base" on an airfield, carrier or farp. --- Carefully plan where each Squadron will be located as part of the defense system. +-- Squadrons are placed at the **home base** on an **airfield**, **carrier** or **farp**. +-- Carefully plan where each Squadron will be located as part of the defense system required for mission effective defenses. +-- If the home base of the squadron is too far from assumed enemy positions, then the defenses will be too late. +-- The home bases must be **behind** enemy lines, you want to prevent your home bases to be engaged by enemies! +-- Depending on the units applied for defenses, the home base can be further or closer to the enemies. -- Any airbase, farp or carrier can act as the launching platform for A2G defenses. --- Carefully plan which airbases will take part in the coalition. Color each airbase in the color of the coalition. +-- Carefully plan which airbases will take part in the coalition. Color each airbase **in the color of the coalition**, using the mission editor, +-- or your air units will not return for landing at the airbase! -- -- -- ## 8. Which helicopter or plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron? @@ -3261,8 +3282,6 @@ do -- AI_A2G_DISPATCHER local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) local IsCas = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == true ) -- Is the AttackerSet a CAS group? - self:F( { Friendlies = self.Detection:GetFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) } ) - if IsCas == true then -- First, count the active defenders, engaging the DetectedItem. @@ -3331,9 +3350,9 @@ do -- AI_A2G_DISPATCHER for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do local DefenderGroup = DefenderGroup -- Wrapper.Group#GROUP local DefenderTaskFsm = self:GetDefenderTaskFsm( DefenderGroup ) - if DefenderTaskFsm:Is( "LostControl" ) then - self:ClearDefenderTask( DefenderGroup ) - end + --if DefenderTaskFsm:Is( "LostControl" ) then + -- self:ClearDefenderTask( DefenderGroup ) + --end if not DefenderGroup:IsAlive() then self:F( { Defender = DefenderGroup:GetName(), DefenderState = DefenderTaskFsm:GetState() } ) if not DefenderTaskFsm:Is( "Started" ) then diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index fd3029b5a..70dd9813b 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1233,7 +1233,7 @@ do -- DETECTION_BASE -- @param DCS#Unit.Category Category The category of the unit. -- @return #boolean true if there are friendlies nearby function DETECTION_BASE:IsFriendliesNearBy( DetectedItem, Category ) - --self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } ) + self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } ) return ( DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category] ~= nil ) or false end From eea1d56468406630a513d08eb27b03ff53d38e7f Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 9 Jan 2019 21:29:45 +0100 Subject: [PATCH 130/147] Optimizations A2G --- Moose Development/Moose/AI/AI_A2G_BAI.lua | 4 ++++ Moose Development/Moose/AI/AI_A2G_CAS.lua | 2 +- Moose Development/Moose/AI/AI_A2G_Dispatcher.lua | 10 +++++----- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 4 ++++ Moose Development/Moose/Functional/Detection.lua | 2 +- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index 59da48bd8..736dbcf98 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -47,6 +47,10 @@ function AI_A2G_BAI:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAlt -- Inherits from BASE local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_BAI + local RTBSpeedMax = AIGroup:GetSpeedMax() or 9999 + + self:SetRTBSpeed( RTBSpeedMax * 0.50, RTBSpeedMax * 0.75 ) + return self end diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 63562487b..8cc974b7a 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -47,7 +47,7 @@ function AI_A2G_CAS:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAlt -- Inherits from BASE local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_CAS - local RTBSpeedMax = AIGroup:GetSpeedMax() + local RTBSpeedMax = AIGroup:GetSpeedMax() or 9999 self:SetRTBSpeed( RTBSpeedMax * 0.50, RTBSpeedMax * 0.75 ) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 35729f0b3..34988ce7d 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3026,7 +3026,7 @@ do -- AI_A2G_DISPATCHER for DefenderID, Defender in pairs( Defenders or {} ) do local Fsm = self:GetDefenderTaskFsm( Defender ) - Fsm:__Engage( 1, AttackerDetection.Set ) -- Engage on the TargetSetUnit + Fsm:Engage( AttackerDetection.Set ) -- Engage on the TargetSetUnit self:SetDefenderTaskTarget( Defender, AttackerDetection ) @@ -3055,7 +3055,7 @@ do -- AI_A2G_DISPATCHER local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) - Fsm:__Engage( 1, AttackerSet ) -- Engage on the TargetSetUnit + Fsm:Engage( AttackerSet ) -- Engage on the TargetSetUnit self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) @@ -3152,9 +3152,9 @@ do -- AI_A2G_DISPATCHER DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead - local AI_A2G_ENGAGE = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + local AI_A2G = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } - local Fsm = AI_A2G_ENGAGE[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude ) -- AI.AI_A2G_ENGAGE + local Fsm = AI_A2G[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude ) -- AI.AI_A2G_ENGAGE Fsm:SetDispatcher( self ) Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) @@ -3175,7 +3175,7 @@ do -- AI_A2G_DISPATCHER self:F( { DefenderTarget = DefenderTarget } ) if DefenderTarget then - Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit + Fsm:Engage( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index 2da77818a..142d73e6a 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -97,6 +97,10 @@ function AI_A2G_SEAD:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAl -- Inherits from BASE local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_SEAD + local RTBSpeedMax = AIGroup:GetSpeedMax() or 9999 + + self:SetRTBSpeed( RTBSpeedMax * 0.50, RTBSpeedMax * 0.75 ) + return self end diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 70dd9813b..087a02207 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1233,7 +1233,7 @@ do -- DETECTION_BASE -- @param DCS#Unit.Category Category The category of the unit. -- @return #boolean true if there are friendlies nearby function DETECTION_BASE:IsFriendliesNearBy( DetectedItem, Category ) - self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } ) +-- self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } ) return ( DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category] ~= nil ) or false end From 8e66031afecdf22e848b7e52a739e5f954de3460 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 9 Jan 2019 21:30:21 +0100 Subject: [PATCH 131/147] Optimizations A2G From 382dfd797c71724df2f69a3ce12eb27ff40406a4 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 9 Jan 2019 21:31:22 +0100 Subject: [PATCH 132/147] Optimizations A2G --- Moose Development/Moose/AI/AI_A2G_BAI.lua | 4 + Moose Development/Moose/AI/AI_A2G_CAS.lua | 2 +- .../Moose/AI/AI_A2G_Dispatcher.lua | 97 +++++++++++-------- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 4 + .../Moose/Functional/Detection.lua | 2 +- 5 files changed, 68 insertions(+), 41 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index 59da48bd8..736dbcf98 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -47,6 +47,10 @@ function AI_A2G_BAI:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAlt -- Inherits from BASE local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_BAI + local RTBSpeedMax = AIGroup:GetSpeedMax() or 9999 + + self:SetRTBSpeed( RTBSpeedMax * 0.50, RTBSpeedMax * 0.75 ) + return self end diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 63562487b..8cc974b7a 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -47,7 +47,7 @@ function AI_A2G_CAS:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAlt -- Inherits from BASE local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_CAS - local RTBSpeedMax = AIGroup:GetSpeedMax() + local RTBSpeedMax = AIGroup:GetSpeedMax() or 9999 self:SetRTBSpeed( RTBSpeedMax * 0.50, RTBSpeedMax * 0.75 ) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index facc45a66..34988ce7d 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -46,13 +46,14 @@ -- ## 1. Which coalition am I modeling an A2G defense system for? blue or red? -- -- One AI_A2G_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. --- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI_A2G_DISPATCHER **objects**, +-- If you want to create a **mutual defense system**, for both blue and red, then you need to instantiate **two** AI_A2G_DISPATCHER **objects**, -- each governing their defense system for one coalition. -- -- -- ## 2. Which type of detection will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). -- --- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units and reporting them to the head quarters. +-- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units +-- and reporting them to the head quarters. -- Several types of @{Functional.Detection} classes exist, and the most common characteristics of these classes is that they: -- -- * Perform detections from multiple recce as one co-operating entity. @@ -61,62 +62,82 @@ -- * Communicates detections. -- -- --- ## 3. Which recce units can be used as part of the detection system? Only Ground or also Airborne? +-- ## 3. Which recce units can be used as part of the detection system? Only ground based, or also airborne? -- --- Depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets. --- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range. --- Recce are very useful to acquire the position of enemy ground targets when spread out over the battlefield at strategic positions. --- Ground units also have varying detectors, and especially the ground units which have laser guiding missiles can be extremely effective at +-- Depending on the type of mission you want to achieve, different types of units can be engaged to perform ground enemy targets reconnaissance. +-- Ground recce (FAC) are very useful units to determine the position of enemy ground targets when they spread out over the battlefield at strategic positions. +-- Using their varying detection technology, and especially those ground units which have spotting technology, can be extremely effective at -- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. +-- Unfortunately, they lack sometimes the visibility to detect targets at greater range, or when scenery is preventing line of sight. -- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then -- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! -- --- Beside ground level units to use for reconnaissance, air units are also very effective. The are capable of patrolling at great speed --- covering a large terrain. However, airborne recce can be vulnerable to air to ground attacks, and you need air superiority to make then --- effective. Also the instruments available at the air units play a big role in the effectiveness of the reconnaissance. --- Air units which have ground detection capabilities will be much more effective than air units with only visual detection capabilities. --- For the red coalition, the Mi-28N and for the blue side, the reaper are such effective reconnaissance airborne units. +-- Airborne recce (AFAC) are also very effective. The are capable of patrolling at a functional detection altitude, +-- having an overview of the whole battlefield. However, airborne recce can be vulnerable to air to ground attacks, +-- so you need air superiority to make them effective. +-- Airborne recce will also have varying ground detection technology, which plays a big role in the effectiveness of the reconnaissance. +-- Certain helicopter or plane types have ground searching radars or advanced ground scanning technology, and are very effective +-- compared to air units having only visual detection capabilities. +-- For example, for the red coalition, the Mi-28N and the Su-34; and for the blue side, the reaper, are such effective airborne recce units. +-- +-- Typically, don't want these recce units to engage with the enemy, you want to keep them at position. Therefore, it is a good practice +-- to set the ROE for these recce to hold weapons, and make them invisible from the enemy. +-- +-- It is not possible to perform a recce function as a player (unit). -- -- --- ## 4. How do defenses decide to engage on approaching enemy units? +-- ## 4. How do the defenses decide **when and where to engage** on approaching enemy units? -- --- The A2G dispacher needs you to setup defense coordinates, which are specific coordinates that are strategic positions in the battle field --- to be defended. Any ground based enemy approaching to such a defense point, will be engaged for defense by A2G defense units. --- The A2G dispatcher provides parameters to setup the defensiveness, meaning, when actually A2G units will engage with the approaching enemy. --- For this, a probability distribution model has been created, which models an increased probability that a defense will engage an attacker, --- depending on the distance of the attacker to the defense coordinate. There are 3 levels of defense reactivity setup, which are Low, Medium and High. --- Defenses will start to consider defensive action when an enemy ground unit is within 60km from a defense point, by default. --- But you can change this maximum distance using on of the available methods. The close the attacker is to the defense point, the --- higher the probability will be that a defense action will be launched! +-- The A2G dispacher needs you to setup (various) defense coordinates, which are strategic positions in the battle field to be defended. +-- Any ground based enemy approaching within the proximity of such a defense point, may trigger for a defensive action by friendly air units. +-- +-- There are 2 important parameters that play a role in the defensive decision making: defensiveness and reactivity. +-- +-- The A2G dispatcher provides various parameters to setup the **defensiveness**, +-- which models the decision **when** a defender will engage with the approaching enemy. +-- Defensiveness is calculated by a probability distribution model when to trigger a defense action, +-- depending on the distance of the enemy unit from the defense coordinates, and a **defensiveness factor**. +-- +-- The other parameter considered for defensive action is **where the enemy is located**, thus the distance from a defense coordinate, +-- which we call the **reactive distance**. By default, the reactive distance is set to 60km, but can be changed by the mission designer +-- using the available method explained further below. +-- The combination of the defensiveness and reactivity results in a model that, the closer the attacker is to the defense point, +-- the higher the probability will be that a defense action will be launched! -- -- -- ## 5. Are defense coordinates and defense reactivity the only parameters? -- -- No, depending on the target type, and the threat level of the target, the probability of defense will be higher. -- In other words, when a SAM-10 radar emitter is detected, its probabilty for defense will be much higher than when a BMP-1 vehicle is --- detected, even when both are at the same distance from a defense coordinate. --- This will ensure optimal defenses, SEAD tasks will be much more quicker launched agains radar emitters, to ensure air superiority. --- Approaching main battle tanks will be much faster defended upon, than a group of approaching trucks. +-- detected, even when both enemies are at the same distance from a defense coordinate. +-- This will ensure optimal defenses, SEAD tasks will be launched much more quicker against engaging radar emitters, to ensure air superiority. +-- Approaching main battle tanks will be engaged much faster, than a group of approaching trucks. -- -- -- ## 6. Which Squadrons will I create and which name will I give each Squadron? -- -- The A2G defense system works with **Squadrons**. Each Squadron must be given a unique name, that forms the **key** to the squadron. --- Several options and activities can be set per Squadron. +-- Several options and activities can be set per Squadron. A free format name can be given, but always ensure that the name is meaningfull +-- for your mission, and remember that squadron names are used for communication to the players of your mission. -- --- There are mainly 3 types of defenses: SEAD, CAS and BAI. +-- There are mainly 3 types of defenses: **SEAD**, **CAS** and **BAI**. -- -- Suppression of Air Defenses (SEAD) are effective agains radar emitters. Close Air Support (CAS) is launched when the enemy is close near friendly units. -- Battleground Air Interdiction (BAI) tasks are launched when there are no friendlies around. -- -- Depending on the defense type, different payloads will be needed. See further points on squadron definition. -- +-- -- ## 7. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? -- --- Squadrons are placed as the "home base" on an airfield, carrier or farp. --- Carefully plan where each Squadron will be located as part of the defense system. +-- Squadrons are placed at the **home base** on an **airfield**, **carrier** or **farp**. +-- Carefully plan where each Squadron will be located as part of the defense system required for mission effective defenses. +-- If the home base of the squadron is too far from assumed enemy positions, then the defenses will be too late. +-- The home bases must be **behind** enemy lines, you want to prevent your home bases to be engaged by enemies! +-- Depending on the units applied for defenses, the home base can be further or closer to the enemies. -- Any airbase, farp or carrier can act as the launching platform for A2G defenses. --- Carefully plan which airbases will take part in the coalition. Color each airbase in the color of the coalition. +-- Carefully plan which airbases will take part in the coalition. Color each airbase **in the color of the coalition**, using the mission editor, +-- or your air units will not return for landing at the airbase! -- -- -- ## 8. Which helicopter or plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron? @@ -3005,7 +3026,7 @@ do -- AI_A2G_DISPATCHER for DefenderID, Defender in pairs( Defenders or {} ) do local Fsm = self:GetDefenderTaskFsm( Defender ) - Fsm:__Engage( 1, AttackerDetection.Set ) -- Engage on the TargetSetUnit + Fsm:Engage( AttackerDetection.Set ) -- Engage on the TargetSetUnit self:SetDefenderTaskTarget( Defender, AttackerDetection ) @@ -3034,7 +3055,7 @@ do -- AI_A2G_DISPATCHER local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) - Fsm:__Engage( 1, AttackerSet ) -- Engage on the TargetSetUnit + Fsm:Engage( AttackerSet ) -- Engage on the TargetSetUnit self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) @@ -3131,9 +3152,9 @@ do -- AI_A2G_DISPATCHER DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead - local AI_A2G_ENGAGE = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + local AI_A2G = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } - local Fsm = AI_A2G_ENGAGE[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude ) -- AI.AI_A2G_ENGAGE + local Fsm = AI_A2G[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude ) -- AI.AI_A2G_ENGAGE Fsm:SetDispatcher( self ) Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) @@ -3154,7 +3175,7 @@ do -- AI_A2G_DISPATCHER self:F( { DefenderTarget = DefenderTarget } ) if DefenderTarget then - Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit + Fsm:Engage( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end @@ -3261,8 +3282,6 @@ do -- AI_A2G_DISPATCHER local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) local IsCas = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == true ) -- Is the AttackerSet a CAS group? - self:F( { Friendlies = self.Detection:GetFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) } ) - if IsCas == true then -- First, count the active defenders, engaging the DetectedItem. @@ -3331,9 +3350,9 @@ do -- AI_A2G_DISPATCHER for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do local DefenderGroup = DefenderGroup -- Wrapper.Group#GROUP local DefenderTaskFsm = self:GetDefenderTaskFsm( DefenderGroup ) - if DefenderTaskFsm:Is( "LostControl" ) then - self:ClearDefenderTask( DefenderGroup ) - end + --if DefenderTaskFsm:Is( "LostControl" ) then + -- self:ClearDefenderTask( DefenderGroup ) + --end if not DefenderGroup:IsAlive() then self:F( { Defender = DefenderGroup:GetName(), DefenderState = DefenderTaskFsm:GetState() } ) if not DefenderTaskFsm:Is( "Started" ) then diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index 2da77818a..142d73e6a 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -97,6 +97,10 @@ function AI_A2G_SEAD:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAl -- Inherits from BASE local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_SEAD + local RTBSpeedMax = AIGroup:GetSpeedMax() or 9999 + + self:SetRTBSpeed( RTBSpeedMax * 0.50, RTBSpeedMax * 0.75 ) + return self end diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index fd3029b5a..087a02207 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1233,7 +1233,7 @@ do -- DETECTION_BASE -- @param DCS#Unit.Category Category The category of the unit. -- @return #boolean true if there are friendlies nearby function DETECTION_BASE:IsFriendliesNearBy( DetectedItem, Category ) - --self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } ) +-- self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } ) return ( DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category] ~= nil ) or false end From 47dd068655fb88f94b2f675f19dee0ec1e1cd870 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 10 Jan 2019 17:26:37 +0100 Subject: [PATCH 133/147] AIRBOSS v0.8.0 --- Moose Development/Moose/Core/Spawn.lua | 65 +- Moose Development/Moose/Ops/Airboss.lua | 1789 ++++++++++++----- .../Moose/Ops/RecoveryTanker.lua | 51 +- Moose Development/Moose/Wrapper/Airbase.lua | 54 +- Moose Development/Moose/Wrapper/Group.lua | 61 +- 5 files changed, 1435 insertions(+), 585 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 63ca1b401..04b1ed8db 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -322,6 +322,9 @@ function SPAWN:New( SpawnTemplatePrefix ) self.Grouping = nil -- No grouping. self.SpawnInitLivery = nil -- No special livery. self.SpawnInitSkill = nil -- No special skill. + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio comms setting. self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -370,7 +373,10 @@ function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) self.Grouping = nil -- No grouping. self.SpawnInitLivery = nil -- No special livery. self.SpawnInitSkill = nil -- No special skill. - + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) @@ -421,7 +427,10 @@ function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPr self.Grouping = nil -- No grouping. self.SpawnInitLivery = nil -- No special livery. self.SpawnInitSkill = nil -- No special skill. - + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else error( "There is no template provided for SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) @@ -597,6 +606,42 @@ function SPAWN:InitSkill( Skill ) return self end +--- Sets the radio comms on or off. Same as checking/unchecking the COMM box in the mission editor. +-- @param #SPAWN self +-- @param #number switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group. +-- @return #SPAWN self +function SPAWN:InitRadioCommsOnOff(switch) + self:F({switch=switch} ) + self.SpawnInitRadio=switch or true + return self +end + +--- Sets the radio frequency of the group. +-- @param #SPAWN self +-- @param #number frequency The frequency in MHz. +-- @return #SPAWN self +function SPAWN:InitRadioFrequency(frequency) + self:F({frequency=frequency} ) + + self.SpawnInitFreq=frequency + + return self +end + +--- Set radio modulation. Default is AM. +-- @param #SPAWN self +-- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM. +-- @return #SPAWN self +function SPAWN:InitRadioModulation(modulation) + self:F({modulation=modulation}) + if modulation and modulation:lower()=="fm" then + self.SpawnInitModu=radio.modulation.FM + else + self.SpawnInitModu=radio.modulation.AM + end + return self +end + --- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups. -- @param #SPAWN self @@ -1174,6 +1219,22 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) end end + + -- Set radio comms on/off. + if self.SpawnInitRadio then + SpawnTemplate.communication=self.SpawnInitRadio + end + + -- Set radio frequency. + if self.SpawnInitFreq then + SpawnTemplate.frequency=self.SpawnInitFreq + end + + -- Set radio modulation. + if self.SpawnInitModu then + SpawnTemplate.modulation=self.SpawnInitModu + end + -- Set country, coaliton and categroy. SpawnTemplate.CategoryID = self.SpawnInitCategory or SpawnTemplate.CategoryID SpawnTemplate.CountryID = self.SpawnInitCountry or SpawnTemplate.CountryID diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 9c03012fb..aac5669d2 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -35,7 +35,7 @@ -- * S-3B Viking & tanker version (AI) -- -- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) as aircraft and the USS John C. Stennis as carrier. --- The A-4E community mod is also supported in priciple but may need further tweaking of parameters. +-- The A-4E community mod is also supported in priciple but may need further tweaking of parameters. Also the A-4E-C mod needs *easy comms* activated to interact with the F10 radio menu. -- -- The implemenation is kept general. So other aircraft and carriers possible in future. [*Winter is coming!*](https://forums.eagle.ru/forumdisplay.php?f=395) -- But each aircraft or carrier needs a different set of optimized individual parameters. @@ -129,6 +129,10 @@ -- @field #number Tcollapse Last time timer.gettime() the stack collapsed. -- @field #AIRBOSS.Recovery recoverywindow Current or next recovery window opened. -- @field #boolean usersoundradio Use user sound output instead of radio transmissions. +-- @field #number Tqueue Last time in seconds of timer.getTime() the queue was updated. +-- @field #number dTqueue Time interval in seconds for updating the queues etc. +-- @field #number dTstatus Time interval for call FSM status updates. +-- @field #boolean menumarkzones If false, disables the option to mark zones via smoke or flares. -- @extends Core.Fsm#FSM --- Be the boss! @@ -264,6 +268,12 @@ -- all flights incoming after 13:15 will be assigned to a Case III marshal stack. Therefore, you should make sure that no flights are incoming long before the -- next window opens or adjust the recovery planning accordingly. -- +-- The following example shows how you set up a recovery window for the next week: +-- +-- for i=0,7 do +-- AddRecoveryWindow(string.format("08:05:00+%d", i), string.format("08:50:00+%d", i)) +-- end +-- -- # The F10 Radio Menu -- -- The F10 radio menu can be used to post requests to Marshal but also provides information about the player and carrier status. Additionally, helper functions @@ -310,7 +320,7 @@ -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuHelp.png) -- --- This menu provides commands to help the player. +-- This menu provides commands to help the player. -- -- ### Mark Zones Submenu -- @@ -400,6 +410,12 @@ -- -- Lists all flights currently in the Marshal queue including their assigned stack, recovery case and Charie time estimate. -- +-- ### Pattern Queue +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuPatternQueue.png) +-- +-- Lists all flights currently in the landing pattern queue showing the time since they entered the pattern. +-- -- # Landing Signal Officer (LSO) -- -- The LSO will first contact you on his radio channel when you are at the the abeam position (Case I) with the phrase "Paddles, contact.". @@ -416,8 +432,8 @@ -- LSO grading starts when the player enters the groove. The flight path and aircraft attitude is evaluated at certain steps (distances measured from rundown): -- -- * **X** At the Start (0.75 NM = 1390 m). --- * **IM** In the Middle (0.375 NM = 695 m). --- * **IC** In Close (0.18 NM = 333 m). +-- * **IM** In the Middle (0.5 NM = 926 m), middle one third of the glideslope. +-- * **IC** In Close (0.25 NM = 463 m), last one third of the glideslope. -- * **AR** At the Ramp (0.027 NM = 50 m). -- * **IW** In the Wiress (at the landing position). -- @@ -446,13 +462,13 @@ -- Line up and glide slope error thresholds were tested extensively using [VFA-113 Stingers LSO Mod](https://forums.eagle.ru/showthread.php?t=211557), -- if the aircraft is outside the red box. In the picture above, **blue** numbers denote the line up thresholds while the **blacks** refer to the glide slope. -- --- A wave off is called, when the aircraft is outside the red rectangle. The measurement stops already 50 m before the rundown, since the error in the calculation +-- A wave off is called, when the aircraft is outside the red rectangle. The measurement stops already ~50 m before the rundown, since the error in the calculation -- increases the closer the aircraft gets to the origin/reference point. -- --- The optimal glide slope is assumed to be 3.5 degrees leading to a touch down point between the second and third wire. +-- The optimal glideslope is assumed to be 3.5 degrees leading to a touch down point between the second and third wire. -- The height of the carrier deck and the exact wire locations are taken into account in the calculations. -- --- ## Pattern Wave Off +-- ## Pattern Waveoff -- -- The player's aircraft position is evaluated at certain critical locations in the landing pattern. If the player is far off from the ideal approach, the LSO will -- issue a pattern wave off. Currently, this is only implemented for Case I recoveries and the Case I part in the Case II recovery, i.e. @@ -474,7 +490,7 @@ -- * 5.0 Points **\_OK\_**: "Okay underline", given only for a perfect pass, i.e. when no deviations at all were observed by the LSO. The unicorn! -- * 4.0 Points **OK**: "Okay pass" when only minor () deviations happend. -- * 3.0 Points **(OK)**: "Fair pass", when only "normal" deviations were detected. --- * 2.0 Points **--**: "No grade, for larger deviations. +-- * 2.0 Points **--**: "No grade", for larger deviations. -- -- Furthermore, we have the cases: -- @@ -482,6 +498,13 @@ -- * 1.0 Points **WO**: "Wave-Off": Player got waved off in the final parts of the groove. -- * 1.0 Points **PWO**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern. For example, being long in the groove gives a "LIG PWO". -- * 0.0 Point **CUT**: "Cut pass", when player was waved off but landed anyway. +-- +-- ## Foul Deck Waveoff +-- +-- A foul deck waveoff is called by the LSO if an aircraft is detected within the landing area when an approaching aircraft is crossing the ship's wake during Case I/II operations, +-- or with an aircraft approaching the 3/4 NM during Case III operations. +-- +-- The approaching aircraft will be notified via radio comms an is supposed to overfly the landing area and enter the Bolter pattern. **The pass is not graded**. -- -- # AI Handling -- @@ -532,76 +555,80 @@ -- -- @field #AIRBOSS AIRBOSS = { - ClassName = "AIRBOSS", - Debug = false, - lid = nil, - carrier = nil, - carriertype = nil, - carrierparam = {}, - alias = nil, - airbase = nil, - waypoints = {}, - currentwp = nil, - beacon = nil, - TACANon = nil, - TACANchannel = nil, - TACANmode = nil, - TACANmorse = nil, - ICLSon = nil, - ICLSchannel = nil, - ICLSmorse = nil, - LSORadio = nil, - LSOFreq = nil, - LSOModu = nil, - MarshalRadio = nil, - MarshalFreq = nil, - MarshalModu = nil, - radiotimer = nil, - zoneCCA = nil, - zoneCCZ = nil, - players = {}, - menuadded = {}, - BreakEntry = {}, - BreakEarly = {}, - BreakLate = {}, - Abeam = {}, - Ninety = {}, - Wake = {}, - Final = {}, - Groove = {}, - Platform = {}, - DirtyUp = {}, - Bullseye = {}, - defaultcase = nil, - case = nil, - defaultoffset = nil, - holdingoffset = nil, - recoverytimes = {}, - flights = {}, - Qpattern = {}, - Qmarshal = {}, - RQMarshal = {}, - RQLSO = {}, - Nmaxpattern = nil, - handleai = nil, - tanker = nil, - warehouse = nil, - Corientation = nil, - Corientlast = nil, - Cposition = nil, - defaultskill = nil, - adinfinitum = nil, - magvar = nil, - Tcollapse = nil, + ClassName = "AIRBOSS", + Debug = false, + lid = nil, + carrier = nil, + carriertype = nil, + carrierparam = {}, + alias = nil, + airbase = nil, + waypoints = {}, + currentwp = nil, + beacon = nil, + TACANon = nil, + TACANchannel = nil, + TACANmode = nil, + TACANmorse = nil, + ICLSon = nil, + ICLSchannel = nil, + ICLSmorse = nil, + LSORadio = nil, + LSOFreq = nil, + LSOModu = nil, + MarshalRadio = nil, + MarshalFreq = nil, + MarshalModu = nil, + radiotimer = nil, + zoneCCA = nil, + zoneCCZ = nil, + players = {}, + menuadded = {}, + BreakEntry = {}, + BreakEarly = {}, + BreakLate = {}, + Abeam = {}, + Ninety = {}, + Wake = {}, + Final = {}, + Groove = {}, + Platform = {}, + DirtyUp = {}, + Bullseye = {}, + defaultcase = nil, + case = nil, + defaultoffset = nil, + holdingoffset = nil, + recoverytimes = {}, + flights = {}, + Qpattern = {}, + Qmarshal = {}, + RQMarshal = {}, + RQLSO = {}, + Nmaxpattern = nil, + handleai = nil, + tanker = nil, + warehouse = nil, + Corientation = nil, + Corientlast = nil, + Cposition = nil, + defaultskill = nil, + adinfinitum = nil, + magvar = nil, + Tcollapse = nil, recoverywindow = nil, usersoundradio = nil, + Tqueue = nil, + dTqueue = nil, + dTstatus = nil, + menumarkzones = nil, } --- Player aircraft types capable of landing on carriers. -- @type AIRBOSS.AircraftPlayer -- @field #string AV8B AV-8B Night Harrier (not yet supported). -- @field #string HORNET F/A-18C Lot 20 Hornet. --- @field #string A4EC Community A-4E-C mod. +-- @field #string A4EC A-4E Community mod. AIRBOSS.AircraftPlayer={ --AV8B="AV8BNA", HORNET="FA-18C_hornet", @@ -611,22 +638,26 @@ AIRBOSS.AircraftPlayer={ --- Aircraft types capable of landing on carrier (human+AI). -- @type AIRBOSS.AircraftCarrier -- @field #string AV8B AV-8B Night Harrier (not yet supported). --- @field #string HORNET F/A-18C Lot 20 Hornet. --- @field #string A4EC Community A-4E mod. +-- @field #string A4EC A-4E Community mod. +-- @field #string HORNET F/A-18C Lot 20 Hornet by Eagle Dynamics. +-- @field #string F14A F-14A by Heatblur. +-- @field #string F14B F-14B by Heatblur. +-- @field #string F14A_AI F-14A Tomcat (AI). +-- @field #string FA18C F/A-18C Hornet (AI). -- @field #string S3B Lockheed S-3B Viking. -- @field #string S3BTANKER Lockheed S-3B Viking tanker. -- @field #string E2D Grumman E-2D Hawkeye AWACS. --- @field #string FA18C F/A-18C Hornet (AI). --- @field #string F14A F-14A Tomcat (AI). AIRBOSS.AircraftCarrier={ --AV8B="AV8BNA", HORNET="FA-18C_hornet", A4EC="A-4E-C", + F14A="F-14A_tomcat", + F14B="F-14B_tomcat", + F14A_AI="F-14A", + FA18C="F/A-18C", S3B="S-3B", S3BTANKER="S-3B Tanker", E2D="E-2C", - FA18C="F/A-18C", - F14A="F-14A", } --- Carrier types. @@ -692,6 +723,7 @@ AIRBOSS.CarrierType={ -- @field #string GROOVE_IC "Groove In Close". -- @field #string GROOVE_AR "Groove At the Ramp". -- @field #string GROOVE_IW "Groove In the Wires". +-- @field #string BOLTER "Bolter Pattern". -- @field #string DEBRIEF "Debrief". AIRBOSS.PatternStep={ UNDEFINED="Undefined", @@ -717,6 +749,7 @@ AIRBOSS.PatternStep={ GROOVE_IC="Groove In Close", GROOVE_AR="Groove At the Ramp", GROOVE_IW="Groove In the Wires", + BOLTER="Bolter Pattern", DEBRIEF="Debrief", } @@ -761,6 +794,7 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall WAVEOFF "Wave off" call. -- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call -- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove" call. +-- @field #AIRBOSS.RadioCall FOULDECK "Foul Deck" call. -- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call. -- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard" call. -- @field #AIRBOSS.RadioCall N0 "Zero" call. @@ -865,6 +899,13 @@ AIRBOSS.LSOCall={ subtitle="You're long in the groove", duration=1.2, }, + FOULDECK={ + file="LSO-FoulDeck", + suffix="ogg", + loud=false, + subtitle="Foul deck", + duration=0.62, + }, DEPARTANDREENTER={ file="LSO-DepartAndReenter", suffix="ogg", @@ -1126,10 +1167,9 @@ AIRBOSS.Difficulty={ -- @field #string groupname Name of the group. -- @field #number nunits Number of units in group. -- @field #number dist0 Distance to carrier in meters when the group was first detected inside the CCA. --- @field #number time Time the flight was added to the queue. +-- @field #number time Timestamp in seconds of timer.AbsTime() of the last important event, e.g. added to the queue. -- @field Core.UserFlag#USERFLAG flag User flag for triggering events for the flight. --- @field #boolean ai If true, flight is AI. --- @field #boolean player If true, flight is a human player. +-- @field #boolean ai If true, flight is purly AI. -- @field #string actype Aircraft type name. -- @field #table onboardnumbers Onboard numbers of aircraft in the group. -- @field #string onboard Onboard number of player or first unit in group. @@ -1147,6 +1187,7 @@ AIRBOSS.Difficulty={ -- @field #boolean ai If true, AI sits inside. If false, human player is flying. -- @field #string onboard Onboard number of the aircraft. -- @field #boolean ballcall If true, flight called the ball in the groove. +-- @field #boolean ai If true, element is AI. --- Player data table holding all important parameters of each player. -- @type AIRBOSS.PlayerData @@ -1165,14 +1206,16 @@ AIRBOSS.Difficulty={ -- @field #boolean landed If true, player landed or attempted to land. -- @field #boolean boltered If true, player boltered. -- @field #boolean waveoff If true, player was waved off during final approach. --- @field #boolean patternwo If true, player was waved of during the pattern. +-- @field #boolean patternwo If true, player was waved off during the pattern. -- @field #boolean lig If true, player was long in the groove. +-- @field #boolean fouldeckwo If true, player was waved off because of a foul deck. -- @field #number Tlso Last time the LSO gave an advice. -- @field #number Tgroove Time in the groove in seconds. -- @field #number wire Wire caught by player when trapped. -- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. -- @field #table points Points of passes until finally landed. -- @field #number finalscore Final score if points are averaged over multiple passes. +-- @field #boolean valid If true, player made a valid approach. Is set true on start of Groove X. -- @extends #AIRBOSS.FlightGroup --- Main radio menu. @@ -1181,12 +1224,14 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.7.2" +AIRBOSS.version="0.8.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Maybe do an additional step at the initial (Case II) or bullseye (Case III) and register player in case he missed some steps. +-- TODO: What happens when section lead or member dies. -- TODO: Include recovery tanker into next stack calculation. Angels six should be empty. -- TODO: Player eject and crash debrief "gradings". -- TODO: Subtitles off options on player level. @@ -1194,6 +1239,7 @@ AIRBOSS.version="0.7.2" -- TODO: Option to filter AI groups for recovery. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! -- TODO: Persistence of results. +-- DONE: Foul deck waveoff. -- DONE: Get Charlie time estimate function. -- DONE: Average player grades until landing. -- DONE: Check player heading at zones, e.g. initial. @@ -1219,7 +1265,7 @@ AIRBOSS.version="0.7.2" -- DONE: Set case II and III times (via recovery time). -- DONE: Get correct wire when trapped. DONE but might need further tweaking. -- DONE: Add radio transmission queue for LSO and airboss. --- TONE: CASE II. +-- DONE: CASE II. -- DONE: CASE III. -- NOPE: Strike group with helo bringing cargo etc. Not yet. -- DONE: Handle crash event. Delete A/C from queue, send rescue helo. @@ -1328,6 +1374,13 @@ function AIRBOSS:New(carriername, alias) -- Carrier patrols its waypoints until the end of time. self:SetPatrolAdInfinitum(true) + -- Set update time intervals. + self:SetQueueUpdateTime() + self:SetStatusUpdateTime() + + -- Menu options + self:SetMenuMarkZones(false) + -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then self:_InitStennis() @@ -1357,7 +1410,8 @@ function AIRBOSS:New(carriername, alias) self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) - BASE:TraceLevel(3) + BASE:TraceLevel(1) + self.dTstatus=0.1 end -- Smoke zones. @@ -1538,7 +1592,7 @@ function AIRBOSS:New(carriername, alias) -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. --- Triggers the delayed FSM event "RecoveryCase" that sets the used aircraft recovery case. - -- @function [parent=#AIRBOSS] __Case + -- @function [parent=#AIRBOSS] __RecoveryCase -- @param #AIRBOSS self -- @param #number delay Delay in seconds. -- @param #number Case The new recovery case (1, 2 or 3). @@ -1648,7 +1702,7 @@ function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset) return self end if Tstop<=Tnow then - self:E(string.format("ERROR: Recovery stop time %s already over. Tnow=%s! Recovery windows rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow))) + self:I(string.format("WARNING: Recovery stop time %s already over. Tnow=%s! Recovery windows rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow))) return self end @@ -1678,6 +1732,22 @@ function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset) return self end +--- Set time interval for updating queues and other stuff. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 30 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetQueueUpdateTime(interval) + self.dTqueue=interval or 30 +end + +--- Set time interval for updating player status and other things. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 0.5 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetStatusUpdateTime(interval) + self.dTstatus=interval or 0.5 +end + --- Disable automatic TACAN activation -- @param #AIRBOSS self -- @return #AIRBOSS self @@ -1685,6 +1755,14 @@ function AIRBOSS:SetTACANoff() self.TACANon=false end +--- Enable or disable F10 radio menu for marking zones via smoke or flares. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, menu is enabled. If false, menu is not available to players. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuMarkZones(switch) + self.menumarkzones=switch or true +end + --- Set TACAN channel of carrier. -- @param #AIRBOSS self -- @param #number channel TACAN channel. Default 74. @@ -1929,16 +2007,7 @@ function AIRBOSS:onafterStart(From, Event, To) if self.ICLSon then self.beacon:ActivateICLS(self.ICLSchannel, self.ICLSmorse) end - - -- Handle events. - self:HandleEvent(EVENTS.Birth) - self:HandleEvent(EVENTS.Land) - self:HandleEvent(EVENTS.Crash) - self:HandleEvent(EVENTS.Ejection) - - -- Time stamp for checking queues. We substract 60 seconds so the routine is called right after status is called the first time. - self.Tqueue=timer.getTime()-60 - + -- Schedule radio queue checks. self.RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQLSO, "LSO"}, 1, 0.01) self.RQMid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQMarshal, "MARSHAL"}, 1, 0.01) @@ -1962,6 +2031,19 @@ function AIRBOSS:onafterStart(From, Event, To) -- Add window. self:AddRecoveryWindow(UTILS.SecondsToClock(Topen), UTILS.SecondsToClock(Tclose)) end + + -- Check Recovery time.s + self:_CheckRecoveryTimes() + + -- Time stamp for checking queues. We substract 60 seconds so the routine is called right after status is called the first time. + self.Tqueue=timer.getTime()-60 + + -- Handle events. + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.Crash) + self:HandleEvent(EVENTS.Ejection) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) -- Start status check in 1 second. self:__Status(1) @@ -1978,7 +2060,7 @@ function AIRBOSS:onafterStatus(From, Event, To) local time=timer.getTime() -- Update marshal and pattern queue every 30 seconds. - if time-self.Tqueue>30 then + if time-self.Tqueue>self.dTqueue then -- Get time. local clock=UTILS.SecondsToClock(timer.getAbsTime()) @@ -1990,6 +2072,9 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Check recovery times and start/stop recovery mode if necessary. self:_CheckRecoveryTimes() + + -- Remove dead/zombie flight groups. Player leaving the server whilst in pattern etc. + --self:_RemoveDeadFlightGroups() -- Scan carrier zone for new aircraft. self:_ScanCarrierZone() @@ -2010,32 +2095,8 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Check AI landing pattern status self:_CheckAIStatus() - -- Call status every 0.5 seconds. - self:__Status(-0.5) -end - ---- Get aircraft nickname. --- @param #AIRBOSS self --- @param #string actype Aircraft type name. --- @return #string Aircraft nickname. E.g. "Hornet" for the F/A-18C or "Tomcat" For the F-14A. -function AIRBOSS:_GetACNickname(actype) - - local nickname="unknown" - if actype==AIRBOSS.AircraftCarrier.A4EC then - nickname="Skyhawk" - elseif actype==AIRBOSS.AircraftCarrier.AV8B then - nickname="Harrier" - elseif actype==AIRBOSS.AircraftCarrier.E2D then - nickname="Hawkeye" - elseif actype==AIRBOSS.AircraftCarrier.F14A then - nickname="Tomcat" - elseif actype==AIRBOSS.AircraftCarrier.FA18C or actype==AIRBOSS.AircraftCarrier.HORNET then - nickname="Hornet" - elseif actype==AIRBOSS.AircraftCarrier.S3B or actype==AIRBOSS.AircraftCarrier.S3BTANKER then - nickname="Viking" - end - - return nickname + -- Call status every ~0.5 seconds. + self:__Status(-self.dTstatus) end --- Check AI status. Pattern queue AI in the groove? Marshal queue AI arrived in holding zone? @@ -2101,7 +2162,7 @@ end function AIRBOSS:_CheckPlayerPatternDistance(player) -- Nothing to do since we check only in the pattern. - if #self.Qpattern==0 then + if #self.Qpattern==0 then return end @@ -2282,6 +2343,7 @@ function AIRBOSS:_CheckRecoveryTimes() -- Debug output. self:T(self.lid..text) + -- Current recovery window. self.recoverywindow=nil -- Carrier is idle. We need to make sure that incoming flights get the correct recovery info of the next window. @@ -2305,7 +2367,7 @@ function AIRBOSS:_CheckRecoveryTimes() end end - self:T({"FF", recoverywindow=self.recoverywindow}) + self:T2({"FF", recoverywindow=self.recoverywindow}) end @@ -2685,7 +2747,7 @@ function AIRBOSS:_InitStennis() self.Abeam.Xmin=-UTILS.NMToMeters(5) -- Not more then 5 NM astern of boat. Should be LIG call anyway. self.Abeam.Xmax= UTILS.NMToMeters(5) -- Not more then 5 NM ahead of boat. self.Abeam.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. - self.Abeam.Zmax= 200 -- Not more than 200 m starboard. Must be port! + self.Abeam.Zmax= 500 -- Not more than 500 m starboard. Must be port! self.Abeam.LimitXmin=-200 -- Check and next step 200 meters behind the ship. self.Abeam.LimitXmax= nil self.Abeam.LimitZmin= nil @@ -2747,12 +2809,13 @@ function AIRBOSS:_GetAircraftAoA(playerData) local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B + local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B -- Table with AoA values. local aoa={} -- #AIRBOSS.AircraftAoA if hornet then - -- F/A-18C Hornet parameters + -- F/A-18C Hornet parameters. aoa.SLOW=9.8 aoa.Slow=9.3 aoa.OnSpeedMax=8.8 @@ -2760,15 +2823,35 @@ function AIRBOSS:_GetAircraftAoA(playerData) aoa.OnSpeedMin=7.4 aoa.Fast=6.9 aoa.FAST=6.3 + elseif tomcat then + -- F-14A/B Tomcat parameters (taken from NATOPS). Converted from units 0-30 to degrees. + -- Currently assuming a linear relationship with 0=-10 degrees and 30=+40 degrees as stated in NATOPS. + aoa.SLOW=18.33 --17.0 units + aoa.Slow=16.67 --16.0 units + aoa.OnSpeedMax=15.83 --15.5 units + aoa.OnSpeed=15.0 --15.0 units + aoa.OnSpeedMin=14.17 --14.5 units + aoa.Fast=13.33 --14.0 units + aoa.FAST=11.67 --13.0 units elseif skyhawk then -- A-4E-C Skyhawk parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 - aoa.SLOW=19.0 - aoa.Slow=18.5 - aoa.OnSpeedMax=18.0 - aoa.OnSpeed=17.5 - aoa.OnSpeedMin=17.0 - aoa.Fast=16.5 - aoa.FAST=16.0 + -- Note that these are arbitrary UNITS and not degrees. We need a conversion formula! + --[[ + aoa.SLOW=self:_AoAUnit2Deg(playerData, 19.0) + aoa.Slow=self:_AoAUnit2Deg(playerData, 18.5) + aoa.OnSpeedMax=self:_AoAUnit2Deg(playerData, 18.0) + aoa.OnSpeed=self:_AoAUnit2Deg(playerData, 17.5) + aoa.OnSpeedMin=self:_AoAUnit2Deg(playerData, 17.0) + aoa.Fast=self:_AoAUnit2Deg(playerData, 16.5) + aoa.FAST=self:_AoAUnit2Deg(playerData, 16.0) + ]] + aoa.SLOW=9.8 + aoa.Slow=9.3 + aoa.OnSpeedMax=8.8 + aoa.OnSpeed=8.1 + aoa.OnSpeedMin=7.4 + aoa.Fast=6.9 + aoa.FAST=6.3 elseif harrier then -- AV-8B Harrier parameters. This might need further tuning. aoa.SLOW=14.0 @@ -2783,6 +2866,81 @@ function AIRBOSS:_GetAircraftAoA(playerData) return aoa end +--- Convert AoA from arbitrary units to degrees. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number aoaunits AoA in arbitrary units. +-- @return #number AoA in degrees. +function AIRBOSS:_AoAUnit2Deg(playerData, aoaunits) + + -- Init. + local degrees=aoaunits + + -- Check aircraft type of player. + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + -- F-14A/B + + -- NATOPS: + -- unit=0 ==> alpha=-10 degrees. + -- unit=30 ==> alpha=+40 degrees. + + -- Assuming a linear relationship between these to points of the graph. + degrees=-10+50/30*aoaunits + + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + -- A-4E + + -- Assuming same conversion as for the Tomcat. Indexer also goes from 0-30 units. Maybe it is right, maybe not. + local a=1.9/5.5 + local a=1/3 + local b=8.0-17.5*a + degrees=a*aoaunits+b + + end + + return degrees +end + +--- Convert AoA from degrees to arbitrary units. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number degrees AoA in degrees. +-- @return #number AoA in arbitrary units. +function AIRBOSS:_AoADeg2Units(playerData, degrees) + + -- Init. + local aoaunits=degrees + + -- Check aircraft type of player. + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + -- F-14A/B + + -- NATOPS: + -- unit=0 ==> alpha=-10 degrees. + -- unit=30 ==> alpha=+40 degrees. + + -- Assuming a linear relationship between these to points of the graph. + aoaunits=(degrees+10)*30/50 + +--[[ + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + -- A-4E + + -- Assuming same conversion as for the Tomcat. Indexer also goes from 0-30 units. Maybe it is right, maybe not. + aoaunits=30/50*degrees+10 + --aoaunits=2*degrees + aoaunits=(degrees-1.406)*5.5/1.9 + + local a=1.9/5.5 + local a=1/3 + local b=8.0-17.5*a + aoaunits=(degrees-b)/a +]] + end + + return aoaunits +end + --- Get optimal aircraft flight parameters at checkpoint. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -2799,6 +2957,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) -- Get AC type. local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC + local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B -- Return values. local alt @@ -2843,7 +3002,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.INITIAL then - if hornet then + if hornet or tomcat then alt=UTILS.FeetToMeters(800) speed=UTILS.KnotsToMps(350) elseif skyhawk then @@ -2853,7 +3012,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.BREAKENTRY then - if hornet then + if hornet or tomcat then alt=UTILS.FeetToMeters(800) speed=UTILS.KnotsToMps(350) elseif skyhawk then @@ -2863,7 +3022,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.EARLYBREAK then - if hornet then + if hornet or tomcat then alt=UTILS.FeetToMeters(800) elseif skyhawk then alt=UTILS.FeetToMeters(600) @@ -2871,7 +3030,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.LATEBREAK then - if hornet then + if hornet or tomcat then alt=UTILS.FeetToMeters(800) elseif skyhawk then alt=UTILS.FeetToMeters(600) @@ -2879,7 +3038,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.ABEAM then - if hornet then + if hornet or tomcat then alt=UTILS.FeetToMeters(600) elseif skyhawk then alt=UTILS.FeetToMeters(500) @@ -2891,7 +3050,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.NINETY then - if hornet then + if hornet or tomcat then alt=UTILS.FeetToMeters(500) elseif skyhawk then alt=UTILS.FeetToMeters(500) @@ -2901,7 +3060,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.WAKE then - if hornet then + if hornet or tomcat then alt=UTILS.FeetToMeters(370) elseif skyhawk then alt=UTILS.FeetToMeters(370) --? @@ -2911,7 +3070,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.FINAL then - if hornet then + if hornet or tomcat then alt=UTILS.FeetToMeters(300) elseif skyhawk then alt=UTILS.FeetToMeters(300) --? @@ -3008,14 +3167,14 @@ function AIRBOSS:_CheckQueue() -- Min time in pattern before next aircraft is allowed. local TpatternMin if pcase==1 then - TpatternMin=1*60*npunits --45*npunits -- 45 seconds interval per plane! + TpatternMin=2*60*npunits --45*npunits -- 45 seconds interval per plane! else TpatternMin=2*60*npunits --120*npunits -- 120 seconds interval per plane! end -- Check recovery window open and enough space to last pattern flight. if self:IsRecovering() and Tpattern>TpatternMin then - self:T(string.format("Sending marshal flight %s to pattern.", marshalflight.groupname)) + self:T(self.lid..string.format("Sending marshal flight %s to pattern.", marshalflight.groupname)) self:_CheckCollapseMarshalStack(marshalflight) end @@ -3115,9 +3274,14 @@ function AIRBOSS:_ScanCarrierZone() end -- Tanker end -- Closed in end -- AI + else - -- Unknown new flight. Create a new flight group. - self:_CreateFlightGroup(group) + + -- Unknown new AI flight. Create a new flight group. + if not self:_IsHuman(group) then + self:_CreateFlightGroup(group) + end + end end @@ -3129,15 +3293,16 @@ function AIRBOSS:_ScanCarrierZone() local flight=_flight --#AIRBOSS.FlightGroup if insideCCA[flight.groupname]==nil then -- Do not remove flights in marshal pattern. At least for case 2 & 3. If zone is set small, they might be outside in the holding pattern. - if not (flight.case>1 and self:_InQueue(self.Qmarshal, flight.group)) then - table.insert(remove, flight.group) + -- TODO: remove only AI flights + if flight.ai and not (self:_InQueue(self.Qmarshal, flight.group) or self:_InQueue(self.Qpattern, flight.group)) then + table.insert(remove, flight) end end end -- Remove flight groups outside CCA. - for _,group in pairs(remove) do - self:_RemoveFlightGroup(group) + for _,flight in pairs(remove) do + self:_RemoveFlightFromQueue(self.flights, flight) end end @@ -3326,7 +3491,7 @@ function AIRBOSS:_LandAI(flight) Speed=UTILS.KnotsToKmph(200) elseif flight.actype==AIRBOSS.AircraftCarrier.E2D then Speed=UTILS.KnotsToKmph(150) - elseif flight.actype==AIRBOSS.AircraftCarrier.F14A then + elseif flight.actype==AIRBOSS.AircraftCarrier.F14A_AI or flight.actype==AIRBOSS.AircraftCarrier.F14B or flight.actype==AIRBOSS.AircraftCarrier.F14B then Speed=UTILS.KnotsToKmph(175) elseif flight.actype==AIRBOSS.AircraftCarrier.S3B or flight.actype==AIRBOSS.AircraftCarrier.S3BTANKER then Speed=UTILS.KnotsToKmph(140) @@ -3488,8 +3653,11 @@ function AIRBOSS:_GetCharlieTime(flightgroup) if flight.holding==nil then -- Flight is also on its way to the marshal stack. - -- TODO: Distance depends on case. - local d0=self:GetCoordinate():Get2DDistance(flight.group:GetCoordinate()) + -- Coordinate of the holding zone. + local holdingzone=self:_GetZoneHolding(flight.case, 1):GetCoordinate() + + -- Distance to holding zone. + local d0=holdingzone:Get2DDistance(flight.group:GetCoordinate()) -- Current velocity. local v0=flight.group:GetVelocityMPS() @@ -3590,7 +3758,8 @@ function AIRBOSS:_CheckCollapseMarshalStack(flight) -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence. if flight.ai then -- Collapse stack and send AI to pattern. - self:_CollapseMarshalStack(flight) + --self:_CollapseMarshalStack(flight) + self:_RemoveFlightFromMarshalQueue(flight, false) self:_LandAI(flight) end @@ -3623,6 +3792,12 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- Stack of flight. local stack=flight.flag:Get() + -- Check that stack > 0. + if stack<=0 then + self:E(self.lid..string.format("ERROR: Flight %s is has stack value %d<0. Cannot collapse stack!", flight.groupname, stack)) + return + end + -- Memorize time when stack collapsed. Should better depend on case but for now we assume there are no two different stacks Case I or II/III. self.Tcollapse=timer.getTime() @@ -3637,16 +3812,14 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) local mstack=mflight.flag:Get() -- Only collapse stacks above the new pattern flight. - -- This will go wrong, if patternflight is not in marshal stack because it will have value -100 and all mstacks will be larger! - -- Maybe need to set the initial value to 1000? Or check stack>0 of pattern flight? - if stack>0 and mstack>stack then + if mstack>stack then -- New stack is old stack minus one. -- TODO: If we include the recovery tanker, this needs to be generalized. local newstack=mstack-1 -- Debug info. - self:T(self.lid..string.format("Flight %s case %d is changing marshal stack %d --> %d.", mflight.groupname, mflight.case, mstack, newstack)) + self:T(self.lid..string.format("Collapse Marshal: Flight %s (case %d) is changing marshal stack %d --> %d.", mflight.groupname, mflight.case, mstack, newstack)) if mflight.ai then @@ -3720,10 +3893,9 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- New time stamp for time in pattern. flight.time=timer.getAbsTime() - - + -- Remove flight from marshal queue. - self:_RemoveGroupFromQueue(self.Qmarshal, flight.group) + --self:_RemoveFlightFromQueue(self.Qmarshal, flight) end @@ -3780,6 +3952,20 @@ function AIRBOSS:_GetQueueInfo(queue, case) local ngroup=0 local nunits=0 + --- Count units of a group which are alive and in the air. + local function countunitsinair(_group) + local group=_group --Wrapper.Group#GROUP + local units=group:GetUnits() + local n=0 + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + if unit:IsAlive() and unit:InAir() then + n=n+1 + end + end + return n + end + -- Loop over flight groups. for _,_flight in pairs(queue) do local flight=_flight --#AIRBOSS.FlightGroup @@ -3789,15 +3975,33 @@ function AIRBOSS:_GetQueueInfo(queue, case) -- Only count specific case with special 23 = CASE II and III combined. if (flight.case==case) or (case==23 and (flight.case==2 or flight.case==3)) then - ngroup=ngroup+1 - nunits=nunits+flight.nunits + + --ngroup=ngroup+1 + --nunits=nunits+flight.nunits + + -- Count alive units in air. + local n=countunitsinair(flight.group) + if n>0 then + ngroup=ngroup+1 + nunits=nunits+n + end + end else -- No specific case requested. Count all groups & units in selected queue. - ngroup=ngroup+1 - nunits=nunits+flight.nunits + --ngroup=ngroup+1 + --nunits=nunits+flight.nunits + + -- Count alive units in air. + local n=countunitsinair(flight.group) + if n>0 then + ngroup=ngroup+1 + nunits=nunits+n + end + + --TODO: add section members? end @@ -3812,30 +4016,24 @@ end -- @param #string name Queue name. function AIRBOSS:_PrintQueue(queue, name) - local nqueue=#queue + --local nqueue=#queue + local Nqueue, nqueue=self:_GetQueueInfo(queue) - local text=string.format("%s Queue N=%d:", name, nqueue) + local text=string.format("%s Queue N=%d, n=%d:", name, Nqueue, nqueue) if nqueue==0 then text=text.." empty." else for i,_flight in pairs(queue) do local flight=_flight --#AIRBOSS.FlightGroup - -- Time stamp. local clock=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) - -- Recovery case of flight. local case=flight.case - -- Stack and stack alt. - local stack=flight.flag:Get() - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, case)) - -- Fuel %. + local stack=flight.flag:Get() local fuel=flight.group:GetFuelMin()*100 - --local fuelstate=self:_GetFuelState(unit) local ai=tostring(flight.ai) - --flight.onboard local lead=flight.seclead local nsec=#flight.section - local actype=flight.actype + local actype=self:_GetACNickname(flight.actype) local onboard=flight.onboard local holding=tostring(flight.holding) @@ -3858,7 +4056,8 @@ function AIRBOSS:_PrintQueue(queue, name) ]] text=text..string.format("\n[%d] %s*%d (%s): lead=%s (%d), onboard=%s, flag=%d, case=%d, time=%s, fuel=%d, ai=%s, holding=%s", i, flight.groupname, flight.nunits, actype, lead, nsec, onboard, stack, case, clock, fuel, ai, holding) - if flight.holding then + if stack>0 then + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, case)) text=text..string.format(" stackalt=%d ft", alt) end end @@ -3919,8 +4118,8 @@ function AIRBOSS:_CreateFlightGroup(group) element.unit=unit element.onboard=flight.onboardnumbers[name] element.ballcall=false - --element.ai= - text=text..string.format("\n[%d] %s onboard #%s", i, name, tostring(element.onboard)) + element.ai=not self:_IsHumanUnit(unit) + text=text..string.format("\n[%d] %s onboard #%s, AI=%s", i, name, tostring(element.onboard), tostring(element.ai)) table.insert(flight.elements, element) end self:T(self.lid..text) @@ -3971,8 +4170,8 @@ function AIRBOSS:_NewPlayer(unitname) playerData.client = CLIENT:FindByName(unitname, nil, true) playerData.seclead = playername - -- Number of passes done by player. - playerData.passes=playerData.passes or 0 + -- Number of passes done by player in this slot. + playerData.passes=0 --playerData.passes or 0 -- LSO grades. playerData.grades=playerData.grades or {} @@ -3992,6 +4191,12 @@ function AIRBOSS:_NewPlayer(unitname) -- Init stuff for this round. playerData=self:_InitPlayer(playerData) + -- Init player data. + self.players[playername]=playerData + + -- Welcome player message. + self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), "AIRBOSS", "", 5) + -- Return player data table. return playerData end @@ -4012,15 +4217,17 @@ function AIRBOSS:_InitPlayer(playerData, step) playerData.debrief={} playerData.warning=nil playerData.holding=nil + playerData.valid=false playerData.lig=false playerData.patternwo=false playerData.waveoff=false + playerData.fouldeckwo=false playerData.boltered=false playerData.landed=false playerData.Tlso=timer.getTime() playerData.Tgroove=nil playerData.wire=nil - --TODO: set flag to -100 here? + playerData.flag:Set(-100) -- Set us up on final if group name contains "Groove". But only for the first pass. if playerData.group:GetName():match("Groove") and playerData.passes==0 then @@ -4082,15 +4289,18 @@ end -- @param #AIRBOSS self -- @param #string unitname Name of the unit. -- @param #AIRBOSS.FlightGroup flight Flight group. +-- @return #boolean If true, element could be removed or nil otherwise. function AIRBOSS:_RemoveFlightElement(unitname, flight) -- Get table index. - local element,idx=self:_GetFlightElement(unitname,flight) + local element,idx=self:_GetFlightElement(unitname, flight) if idx then table.remove(flight.elements, idx) + return true else - self:E("ERROR: Flight element could not be removed from flight group. Index=nil!") + self:T("WARNING: Flight element could not be removed from flight group. Index=nil!") + return nil end end @@ -4110,27 +4320,64 @@ function AIRBOSS:_InQueue(queue, group) return false end - ---- Remove a flight group. +--- Remove dead flight groups from all queues. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. -- @return #AIRBOSS.FlightGroup Flight group. -function AIRBOSS:_RemoveFlightGroup(group) - local groupname=group:GetName() - for i,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.FlightGroup - if flight.groupname==groupname then - self:T(string.format("Removing flight group %s (not in CCA).", groupname)) +function AIRBOSS:_RemoveDeadFlightGroups() + + -- Remove dead flights from all flights table. + for i=#self.flight,1,-1 do + local flight=self.flights[i] --#AIRBOSS.FlightGroup + if not flight.group:IsAlive() then + self:T(string.format("Removing dead flight group %s from ALL flights table.", flight.groupname)) table.remove(self.flights, i) - return - end + end end + + -- Remove dead flights from Marhal queue table. + for i=#self.Qmarshal,1,-1 do + local flight=self.Qmarshal[i] --#AIRBOSS.FlightGroup + if not flight.group:IsAlive() then + self:T(string.format("Removing dead flight group %s from Marshal Queue table.", flight.groupname)) + table.remove(self.Qmarshal, i) + end + end + + -- Remove dead flights from Pattern queue table. + for i=#self.Qpattern,1,-1 do + local flight=self.Qpattern[i] --#AIRBOSS.FlightGroup + if not flight.group:IsAlive() then + self:T(string.format("Removing dead flight group %s from Pattern Queue table.", flight.groupname)) + table.remove(self.Qpattern, i) + end + end + +end + +--- Remove a flight group from the Marshal queue. Marshal stack is collapsed, too, if flight was in the queue. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group that will be removed from queue. +-- @param #boolean nopattern If true, flight is NOT going to landing pattern. +-- @return #boolean If true, flight was removed. +function AIRBOSS:_RemoveFlightFromMarshalQueue(flight, nopattern) + + -- Remove flight from marshal queue if it is in. + local removed=self:_RemoveFlightFromQueue(self.Qmarshal, flight) + + -- Collapse marshal stack if flight was removed. + if removed then + self:_CollapseMarshalStack(flight, nopattern) + end + + return removed end --- Remove a flight group from a queue. -- @param #AIRBOSS self -- @param #table queue The queue from which the group will be removed. -- @param #AIRBOSS.FlightGroup flight Flight group that will be removed from queue. +-- @return #boolean If true, flight was in Queue and removed. function AIRBOSS:_RemoveFlightFromQueue(queue, flight) -- Loop over all flights in group. @@ -4141,34 +4388,11 @@ function AIRBOSS:_RemoveFlightFromQueue(queue, flight) if qflight.groupname==flight.groupname then self:T(self.lid..string.format("Removing flight group %s from queue.", flight.groupname)) table.remove(queue, i) - return - end - end - -end - - ---- Remove a group from a queue. --- @param #AIRBOSS self --- @param #table queue The queue from which the group will be removed. --- @param Wrapper.Group#GROUP group Group that will be removed from queue. -function AIRBOSS:_RemoveGroupFromQueue(queue, group) - - -- Group name. - local name=group:GetName() - - -- Loop over all flights in group. - for i,_flight in pairs(queue) do - local flight=_flight --#AIRBOSS.FlightGroup - - -- Check for name. - if flight.groupname==name then - self:T(self.lid..string.format("Removing group %s from queue.", name)) - table.remove(queue, i) - return + return true end end + return false end --- Remove a unit from a flight group (e.g. when landed) and update all queues if the whole flight group is gone. @@ -4192,35 +4416,41 @@ function AIRBOSS:_RemoveUnitFromFlight(unit) if flight then -- Remove element from flight group. - self:_RemoveFlightElement(unit:GetName(), flight) + local removed=self:_RemoveFlightElement(unit:GetName(), flight) - -- Decrease number of units in group. - flight.nunits=flight.nunits-1 + if removed then - -- Check if numbers still match. - if #flight.elements~=flight.nunits then - self:E("ERROR: Number of elements != number of units in flight!") - end - - -- Check if no units are left. - if flight.nunits==0 then - -- Remove flight from all queues. - self:_RemoveFlight(flight) - end - + -- Decrease number of units in group. + flight.nunits=flight.nunits-1 + + -- Check if numbers still match. + if #flight.elements~=flight.nunits then + self:E("ERROR: Number of elements != number of units in flight!") + end + + -- Check if no units are left. + if flight.nunits==0 then + -- Remove flight from all queues. + self:_RemoveFlight(flight) + end + + end end end end end ---- Remove a flight from all queues. Also set player step to undefined if applicable. +--- Remove a flight from Marshal and Pattern queues. If flight is in Marhal queue, the above stack is collapsed. +-- Also set player step to undefined if applicable or remove human flight if option *completely* is true. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData flight The flight to be removed. -function AIRBOSS:_RemoveFlight(flight) +-- @param #boolean completely If true, also remove human flight from all flights table. +function AIRBOSS:_RemoveFlight(flight, completely) -- Remove flight from all queues. - self:_RemoveFlightFromQueue(self.Qmarshal, flight) + --self:_RemoveFlightFromQueue(self.Qmarshal, flight) + self:_RemoveFlightFromMarshalQueue(flight, true) self:_RemoveFlightFromQueue(self.Qpattern, flight) -- Check if player or AI @@ -4228,8 +4458,13 @@ function AIRBOSS:_RemoveFlight(flight) -- Remove AI flight completely. self:_RemoveFlightFromQueue(self.flights, flight) else - -- Set Playerstep to undefined. - flight.step=AIRBOSS.PatternStep.UNDEFINED + if completely then + -- Remove HUMAN flight completely. + self:_RemoveFlightFromQueue(self.flights, flight) + else + -- Set Playerstep to undefined. + flight.step=AIRBOSS.PatternStep.UNDEFINED + end end end @@ -4376,13 +4611,32 @@ function AIRBOSS:_CheckPlayerStatus() playerData.step==AIRBOSS.PatternStep.GROOVE_IM then --self:_CheckPlayerPatternDistance(playerData) end + + -- Foul deck check. + if playerData.case<3 then + + -- Case I/II: Check is done, when AC is at the wake according to NATOPS. At the wake we switch to final. + if playerData.step==AIRBOSS.PatternStep.FINAL or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX then + + self:_CheckFoulDeck(playerData) + end + + else + + -- Case III: Check is done at 3/4 NM according to NATOPS + if playerData.step==AIRBOSS.PatternStep.GROOVE_XX then + + self:_CheckFoulDeck(playerData) + end + end if playerData.step==AIRBOSS.PatternStep.UNDEFINED then -- Status undefined. - local time=timer.getAbsTime() - local clock=UTILS.SecondsToClock(time) - self:T3(string.format("Player status undefined. Waiting for next step. Time %s", clock)) + --local time=timer.getAbsTime() + --local clock=UTILS.SecondsToClock(time) + --self:T3(string.format("Player status undefined. Waiting for next step. Time %s", clock)) elseif playerData.step==AIRBOSS.PatternStep.REFUELING then @@ -4401,6 +4655,11 @@ function AIRBOSS:_CheckPlayerStatus() -- CASE I/II/III: New approach. self:_Commencing(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.BOLTER then + + -- CASE I/II/III: Bolter pattern. + self:_BolterPattern(playerData) elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then @@ -4539,21 +4798,24 @@ function AIRBOSS:OnEventBirth(EventData) if rightaircraft==false then local text=string.format("Player aircraft type %s not supported by AIRBOSS class.", _unit:GetTypeName()) MESSAGE:New(text, 30):ToAllIf(self.Debug) - self:T(self.lid..text) + self:T2(self.lid..text) return end - + -- Add Menu commands. self:_AddF10Commands(_unitName) -- Init new player data. - local playerData=self:_NewPlayer(_unitName) + --local playerData=self:_NewPlayer(_unitName) + + -- Delaying the new player for a second, because AI units of the flight would not be registered correctly. + SCHEDULER:New(nil, self._NewPlayer, {self, _unitName}, 1) -- Init player data. - self.players[_playername]=playerData + --self.players[_playername]=playerData -- Welcome player message. - self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), "AIRBOSS", "", 5) + --self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), "AIRBOSS", "", 5) end end @@ -4576,6 +4838,13 @@ function AIRBOSS:OnEventLand(EventData) -- This would be the closest airbase. local airbase=EventData.Place + + -- Nil check for airbase. Crashed as player gave me no airbase. + if airbase==nil then + return + end + + -- Get airbase name. local airbasename=tostring(airbase:GetName()) -- Check if aircraft landed on the right airbase. @@ -4584,6 +4853,7 @@ function AIRBOSS:OnEventLand(EventData) -- Stern coordinate at the rundown. local stern=self:_GetSternCoord() + -- Polygon zone close around the carrier. local zoneCarrier=self:_GetZoneCarrierBox() -- Check if player or AI landed. @@ -4603,9 +4873,31 @@ function AIRBOSS:OnEventLand(EventData) -- Player data. local playerData=self.players[_playername] --#AIRBOSS.PlayerData + -- Check if playerData is okay. + if playerData==nil then + self:E(self.lid.."ERROR: playerData nil in landing event.") + return + end + -- Check that player landed on the carrier. if _unit:IsInZone(zoneCarrier) then + -- Check if this was a valid approach. + if not playerData.valid then + -- Player missed at least one step in the pattern. + local text=string.format("You missed at least one important step in the pattern!\nYour next step would have been %s.\nThis pass is INVALID.", playerData.step) + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 30, true, 5) + + -- Reinitialize player data. + self:_InitPlayer(playerData) + + -- Clear queues just in case. + self:_RemoveFlightFromMarshalQueue(playerData, true) + self:_RemoveFlightFromQueue(self.Qpattern, playerData) + + return + end + -- Check if player already landed. We dont need a second time. if playerData.landed then @@ -4667,7 +4959,9 @@ function AIRBOSS:OnEventLand(EventData) else -- TODO: Handle case where player did not land on the carrier. - self:E(self.lid..string.format("Player %s did not land in carrier box zone. Maybe in the water near the carrier?", playerData.name)) + if playerData then + self:E(self.lid..string.format("Player %s did not land in carrier box zone. Maybe in the water near the carrier?", playerData.name)) + end end else @@ -4692,7 +4986,6 @@ function AIRBOSS:OnEventLand(EventData) -- AI always lands ==> remove unit from flight group and queues. self:_RemoveUnitFromFlight(EventData.IniUnit) - end end end @@ -4711,13 +5004,25 @@ function AIRBOSS:OnEventCrash(EventData) self:T3(self.lid.."CARSH: player = "..tostring(_playername)) if _unit and _playername then + -- Debug message. self:T(self.lid..string.format("Player %s crashed!",_playername)) + + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + if flight then + self:_RemoveFlight(flight, true) + end + else - self:T2(self.lid..string.format("AI unit %s crashed!", EventData.IniUnitName)) + -- Debug message. + self:T2(self.lid..string.format("AI unit %s crashed!", EventData.IniUnitName)) + + -- Remove unit from flight and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) end - -- Remove unit from flight and queues. - self:_RemoveUnitFromFlight(EventData.IniUnit) end --- Airboss event handler for event Ejection. @@ -4735,12 +5040,115 @@ function AIRBOSS:OnEventEjection(EventData) if _unit and _playername then self:T(self.lid..string.format("Player %s ejected!",_playername)) + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + if flight then + self:_RemoveFlight(flight, true) + end else - self:T2(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) + -- + self:T2(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) + + -- Remove unit from flight and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) end - -- Remove unit from flight and queues. - self:_RemoveUnitFromFlight(EventData.IniUnit) +end + +--- Airboss event handler for event player leave unit. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +--function AIRBOSS:OnEventPlayerLeaveUnit(EventData) +function AIRBOSS:_PlayerLeft(EventData) + self:F3({eventleave=EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."PLAYERLEAVEUNIT: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."PLAYERLEAVEUNIT: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."PLAYERLEAVEUNIT: player = "..tostring(_playername)) + + if _unit and _playername then + + -- Debug info. + self:T(self.lid..string.format("Player %s left unit %s!",_playername, _unitName)) + + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + if flight then + self:_RemoveFlight(flight, true) + end + + end + +end + +--- General event handler. +-- @param #AIRBOSS self +-- @param #table Event DCS event table. +function AIRBOSS:onEvent(Event) + self:F3(Event) + + if Event == nil or Event.initiator == nil then + self:T3(AIRBOSS.lid.."Skipping onEvent. Event or Event.initiator unknown.") + return true + end + if Unit.getByName(Event.initiator:getName()) == nil then + self:T3(AIRBOSS.lid.."Skipping onEvent. Initiator unit name unknown.") + return true + end + + local DCSiniunit = Event.initiator + + local EventData={} --Core.Event#EVENTDATA + local _playerunit=nil + local _playername=nil + + if Event.initiator then + EventData.IniUnitName = Event.initiator:getName() + EventData.IniDCSGroup = Event.initiator:getGroup() + EventData.IniGroupName = Event.initiator:getGroup():getName() + EventData.IniUnit = UNIT:Find(DCSiniunit) + EventData.IniDCSUnit = Event.initiator + -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. + _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName) + end + + -- Event info. + self:T3(self.lid..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) + self:T3(self.lid..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) + self:T3(self.lid..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) + self:T3(self.lid..string.format("EVENT: Ini player = %s" , tostring(_playername))) + + -- Call event PlayerLeaveUnit function. + if Event.id==world.event.S_EVENT_PLAYER_LEAVE_UNIT and _playername then + --self:OnEventPlayerLeaveUnit(EventData) + self:_PlayerLeft(EventData) + end + + --[[ + + -- Call event Birth function. + if Event.id==world.event.S_EVENT_BIRTH and _playername then + self:OnEventBirth(EventData) + end + + -- Call event Ejection function. + if Event.id==world.event.S_EVENT_EJECTION and _playername then + self:OnEventEjection(EventData) + end + + -- Call event Crash function. + if Event.id==world.event.S_EVENT_CRASH and _playername then + self:OnEventCrash(EventData) + end + + ]] end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4776,11 +5184,11 @@ function AIRBOSS:_Holding(playerData) -- Acceptable altitude depending on player skill. local altgood=UTILS.FeetToMeters(500) if playerData.difficulty==AIRBOSS.Difficulty.HARD then - -- Pros can be expected to be within +-100 ft. - altgood=UTILS.FeetToMeters(100) + -- Pros can be expected to be within +-200 ft. + altgood=UTILS.FeetToMeters(200) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then - -- Normal guys should be within +-300 ft. - altgood=UTILS.FeetToMeters(300) + -- Normal guys should be within +-350 ft. + altgood=UTILS.FeetToMeters(350) elseif playerData.difficulty==AIRBOSS.Difficulty.EASY then -- Students should be within +-500 ft. altgood=UTILS.FeetToMeters(500) @@ -4995,13 +5403,13 @@ function AIRBOSS:_CheckCorridor(playerData) -- Issue warning. if invalid and (not playerData.warning) then - self:MessageToPlayer(playerData, "You left the valid approach corridor!", "MARSHAL") - playerData.warning=true + self:MessageToPlayer(playerData, "You left the approach corridor!", "MARSHAL") + playerData.warning=true end -- Back in zone. if (not invalid) and playerData.warning then - self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARSHAL") + self:MessageToPlayer(playerData, "You're back in the approach corridor.", "MARSHAL") playerData.warning=false end @@ -5200,8 +5608,10 @@ function AIRBOSS:_DirtyUp(playerData) end -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. - self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.SAYNEEDLES, false, 10) - self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.FLYNEEDLES, false, 15) + self:_Number2Radio(self.MarshalRadio, playerData.onboard, 20) + self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.SAYNEEDLES, false, 20.1) + self:_Number2Radio(self.MarshalRadio, playerData.onboard, 25) + self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.FLYNEEDLES, false, 25.1) -- TODO: Make Fly Bullseye call if no automatic ICLS is active. -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). @@ -5262,7 +5672,38 @@ function AIRBOSS:_Bullseye(playerData) self:_StepHint(playerData) end end - + +--- Bolter pattern. Sends player to abeam for Case I/II or Bullseye for Case III ops. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_BolterPattern(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Bolter Pattern thresholds. + local Bolter={} + Bolter.name="Bolter Pattern" + Bolter.Xmin=-UTILS.NMToMeters(5) -- Not more then 5 NM astern of boat. + Bolter.Xmax= UTILS.NMToMeters(3) -- Not more then 3 NM ahead of boat. + Bolter.Zmin=-UTILS.NMToMeters(5) -- Not more than 2 NM port. + Bolter.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + Bolter.LimitXmin= 100 -- Check that 100 meter ahead and port + Bolter.LimitXmax= nil + Bolter.LimitZmin= nil + Bolter.LimitZmax= nil + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, Bolter) then + if playerData.case<3 then + playerData.step=AIRBOSS.PatternStep.ABEAM + else + playerData.step=AIRBOSS.PatternStep.BULLSEYE + end + playerData.warning=nil + self:_StepHint(playerData) + end +end --- Break entry for case I/II recoveries. -- @param #AIRBOSS self @@ -5382,7 +5823,7 @@ function AIRBOSS:_CheckForLongDownwind(playerData) self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.DEPARTANDREENTER) -- Debrief. - self:_AddToDebrief(playerData, "Long in the groove - Pattern Wave Off!") + self:_AddToDebrief(playerData, "Long in the groove - Pattern Waveoff!") --grade="LIG PATTERN WAVE OFF - CUT 1 PT" playerData.lig=true @@ -5614,7 +6055,6 @@ function AIRBOSS:_Final(playerData) end - --- In the groove. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -5661,9 +6101,9 @@ function AIRBOSS:_Groove(playerData) -- Ranges in the groove. local RX0=UTILS.NMToMeters(1.000) -- Everything before X 1.00 = 1852 m local RXX=UTILS.NMToMeters(0.750) -- Start of groove. 0.75 = 1389 m - local RIM=UTILS.NMToMeters(0.375) -- In the Middle 0.75/2 = 0.375 = 695 m - local RIC=UTILS.NMToMeters(0.180) -- In Close. 0.18 = 333 m (0.1=185 m) - local RAR=UTILS.NMToMeters(0.027) -- At the Ramp. 0.027 = 50 m + local RIM=UTILS.NMToMeters(0.500) -- In the Middle 0.50 = 926 m (middle one third of the glideslope) + local RIC=UTILS.NMToMeters(0.250) -- In Close 0.25 = 463 m (last one third of the glideslope) + local RAR=UTILS.NMToMeters(0.040) -- At the Ramp. 0.04 = 75 m -- Groove data. local groovedata={} --#AIRBOSS.GrooveData @@ -5689,6 +6129,9 @@ function AIRBOSS:_Groove(playerData) -- Store data. playerData.groove.XX=groovedata + -- This is a valid approach and player did not miss any important steps in the pattern. + playerData.valid=true + -- Next step: in the middle. playerData.step=AIRBOSS.PatternStep.GROOVE_IM playerData.warning=nil @@ -5775,7 +6218,7 @@ function AIRBOSS:_Groove(playerData) -- For debugging. local text=string.format("Groove %s: LineUp=%.2f GlideSlope=%.2f AoA=%.2f\n", gs, lineupError, glideslopeError, AoA) text=text..string.format("R=%.1f m, h=%.1f m", rho, alt-self.carrierparam.deckheight-2) - MESSAGE:New(text, 1, nil, true):ToAllIf(self.Debug) + --MESSAGE:New(text, 1, nil, true):ToAllIf(self.Debug) -- Check if we are beween 3/4 NM and end of ship. if rho>=RAR and rhomath.abs(gd.LUE) then @@ -5866,7 +6309,7 @@ function AIRBOSS:_Groove(playerData) self:E("What? Player was not waved off but flew past the carrier without landing. Why did waveoff not kick in?") -- TODO: This is more like a pilot wave off then. - self:_AddToDebrief(playerData, "Pilot wave-off.") + self:_AddToDebrief(playerData, "Pilot waveoff.") playerData.waveoff=true @@ -5883,8 +6326,8 @@ end --- LSO check if player needs to wave off. -- Wave off conditions are: -- --- * Glide slope error > 1 degree. --- * Line up error > 3 degrees. +-- * Glide slope error <1.2 or >1.8 degrees. +-- * |Line up error| > 3 degrees. -- * AoA check but only for TOPGUN graduates. -- @param #AIRBOSS self -- @param #number glideslopeError Glide slope error in degrees. @@ -5939,6 +6382,84 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) return waveoff end +--- Check if other aircraft are currently on the landing runway. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return boolean If true, we have a foul deck. +function AIRBOSS:_CheckFoulDeck(playerData) + + -- Landing runway zone. + local runway=self:_GetZoneRunwayBox() + + -- Scan radius. + local R=250 + + -- Debug info. + self:T(self.lid..string.format("Foul deck check: Scanning Carrier Runway Area. Radius=%.1f m.", R)) + + -- Scan units in carrier zone. + local _,_,_,unitscan=self:GetCoordinate():ScanObjects(R, true, false, false) + + -- Loop over all scanned units and check if they are on the runway. + local fouldeck=false + local foulunit=nil --Wrapper.Unit#UNIT + for _,_unit in pairs(unitscan) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Check if unit is in zone. + local inzone=unit:IsInZone(runway) + + -- Check if aircraft and in air. + local isaircraft=unit:IsAir() + local isairborn =unit:InAir() + + if inzone and isaircraft and not isairborn then + local text=string.format("Unit %s on landing runway ==> Foul deck!", unit:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + --runway:FlareZone(FLARECOLOR.Red, 30) + fouldeck=true + foulunit=unit + end + end + + + -- Add to debrief and + if playerData and fouldeck then + + -- Debrief text. + local text=string.format("Foul deck waveoff due to aircraft %s!", foulunit:GetName()) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + + -- Foul deck + wave off radio message. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.FOULDECK, false, 1) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WAVEOFF, false, 1) + + -- Player hint for flight students. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + local text=string.format("overfly landing area and enter bolter pattern.") + self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 3) + end + + -- Set player parameters for foul deck + playerData.fouldeckwo=true + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil + playerData.valid=false + + -- Send a message to the player that blocks the runway. + if foulunit then + local foulflight=self:_GetFlightFromGroupInQueue(foulunit:GetGroup(), self.flights) + if foulflight and not foulflight.ai then + self:MessageToPlayer(foulflight, "Move your ass away from my runway!", "MARSHAL", nil, 10) + end + end + end + + return fouldeck +end + --- Get "stern" coordinate. -- @param #AIRBOSS self -- @return Core.Point#COORDINATE Coordinate at the rundown of the carrier. @@ -6063,10 +6584,16 @@ function AIRBOSS:_Trapped(playerData) -- Get current wire (estimate). This now based on the position where the player comes to a standstill which should reflect the trapped wire better. local dcorr=100 + if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then + dcorr=100 + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + -- A-4E gets slowed down much faster the the F/A-18C! + dcorr=56 + end local wire=self:_GetWire(coord, dcorr) -- Debug. - local text=string.format("Player %s _Trapped: v=%.1f km/h, s=%.1f m ==> wire=%d (dcorr=%d)", playerData.name, v, s, wire, dcorr) + local text=string.format("Player %s _Trapped: v=%.1f km/h, s-dcorr=%.1f m ==> wire=%d (dcorr=%d)", playerData.name, v, s-dcorr, wire, dcorr) self:T(self.lid..text) -- Call this function again until v < threshold. Player comes to a standstill ==> Get wire! @@ -6147,10 +6674,10 @@ function AIRBOSS:_GetZoneInitial(case) if case==1 then -- Case I - local c1=cv:Translate(UTILS.NMToMeters(0.5), radial-90) -- 0.0 0.5 starboard - local c2=c1:Translate(UTILS.NMToMeters(3), radial):Translate(1, radial-90) -- -3.0 1.5 starboard, astern - local c3=c2:Translate(UTILS.NMToMeters(2), radial+90) - local c4=cv:Translate(UTILS.NMToMeters(0.5), radial) + local c1=cv:Translate(UTILS.NMToMeters(0.5), radial-90) -- 0.0 0.5 starboard + local c2=cv:Translate(UTILS.NMToMeters(1.3), radial-90):Translate(UTILS.NMToMeters(3), radial) -- -3.0 1.3 starboard, astern + local c3=cv:Translate(UTILS.NMToMeters(0.4), radial+90):Translate(UTILS.NMToMeters(3), radial) -- -3.0 -0.4 port, astern + local c4=cv:Translate(UTILS.NMToMeters(1.0), radial) local c5=cv -- Vec2 array. @@ -6268,7 +6795,7 @@ function AIRBOSS:_GetZoneArcIn(case) -- Angle between FB/BRC and holding zone. local alpha=math.rad(self.holdingoffset) - -- 12+x NM from carrier + -- 14+x NM from carrier local x=14/math.cos(alpha) -- Distance = 14 NM @@ -6446,7 +6973,7 @@ end --- Get zone of landing runway -- @param #AIRBOSS self --- @return Core.Zone#ZONE Zone surrounding landing runway. +-- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. function AIRBOSS:_GetZoneRunwayBox() -- Stern coordinate. @@ -6459,9 +6986,9 @@ function AIRBOSS:_GetZoneRunwayBox() local p={} -- Points. - p[1]=S:Translate(self.carrierparam.rwywidth, FB+90) + p[1]=S:Translate(self.carrierparam.rwywidth*0.5, FB+90) p[2]=p[1]:Translate(self.carrierparam.rwylength, FB) - p[3]=p[2]:Translate(self.carrierparam.rwywidth*2, FB-90) + p[3]=p[2]:Translate(self.carrierparam.rwywidth, FB-90) p[4]=p[3]:Translate(self.carrierparam.rwylength, FB-180) -- Convert to vec2. @@ -6565,15 +7092,28 @@ function AIRBOSS:_AttitudeMonitor(playerData) -- Relative heading Aircraft to Carrier. local relhead=self:_GetRelativeHeading(playerData.unit) + + local step=playerData.step + if playerData.step==AIRBOSS.PatternStep.GROOVE_X0 or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then + step=self:_GS(step,-1) + end -- Output - local text=string.format("Pattern step: %s\n", playerData.step) - text=text..string.format("AoA=%.1f | |V|=%.1f knots\n", aoa, UTILS.MpsToKnots(vabs)) - text=text..string.format("Vx=%.1f Vy=%.1f Vz=%.1f m/s\n", velo.x, velo.y, velo.z) - text=text..string.format("Pitch=%.1f° | Roll=%.1f° | Yaw=%.1f°\n", pitch, roll, yaw) - text=text..string.format("Climb Angle=%.1f° | Rate=%d ft/min\n", unit:GetClimbAngle(), velo.y*196.85) - text=text..string.format("R=%.2f NM | X=%d Z=%d m\n", UTILS.MetersToNM(rho), dx, dz) - text=text..string.format("Gamma=%.1f°", relhead) + local text=string.format("Pattern step: %s", step) + text=text..string.format("\nAoA=%.1f° | |V|=%.1f knots", aoa, UTILS.MpsToKnots(vabs)) + if self.Debug then + -- Velocity vector. + text=text..string.format("\nVx=%.1f Vy=%.1f Vz=%.1f m/s", velo.x, velo.y, velo.z) + --Wind vector. + text=text..string.format("\nWind Vx=%.1f Vy=%.1f Vz=%.1f m/s", wind.x, wind.y, wind.z) + end + text=text..string.format("\nPitch=%.1f° | Roll=%.1f° | Yaw=%.1f°", pitch, roll, yaw) + text=text..string.format("\nClimb Angle=%.1f° | Rate=%d ft/min", unit:GetClimbAngle(), velo.y*196.85) -- If in the groove, provide line up and glide slope error. if playerData.step==AIRBOSS.PatternStep.GROOVE_X0 or playerData.step==AIRBOSS.PatternStep.GROOVE_XX or @@ -6583,15 +7123,16 @@ function AIRBOSS:_AttitudeMonitor(playerData) playerData.step==AIRBOSS.PatternStep.GROOVE_IW then local lineup=self:_Lineup(playerData.unit, true) local glideslope=self:_Glideslope(playerData.unit, 3.5) - text=text..string.format("\nLineUp=%.2f° | GlideSlope=%.2f°", lineup, glideslope) - --text=text..string.format("\nGlideslope = ", glideslope) + local dist=self:_GetOptLandingCoordinate():Get2DDistance(playerData.unit) + text=text..string.format("\nDist=%.1f m Alt=%.1f m", dist, self:_GetAltCarrier(playerData.unit)) + text=text..string.format("\nLineUp=%.2f° | GlideSlope=%.2f° | AoA=%.1f", lineup, glideslope, self:_AoADeg2Units(playerData, aoa)) local grade, points, analysis=self:_LSOgrade(playerData) - text=text..string.format("\n%s %.1f PT - %s", grade, points, analysis) + text=text..string.format("\nGrade: %s %.1f PT - %s", grade, points, analysis) + else + text=text..string.format("\nR=%.2f NM | X=%d Z=%d m", UTILS.MetersToNM(rho), dx, dz) + text=text..string.format("\nGamma=%.1f°", relhead) end - -- Wind (for debugging). - --text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) - MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) end @@ -6604,22 +7145,15 @@ function AIRBOSS:_Glideslope(unit, optangle) -- Default is 0. optangle=optangle or 0 - - -- Stern coordinate. - local stern=self:_GetSternCoord() - - -- Ideally we want to land between 2nd and 3rd wire. - if self.carrierparam.wire3 then - -- We take the position of the 3rd wire to approximately account for the length of the aircraft. - local d23=self.carrierparam.wire3 --self.carrierparam.wire2+0.5*(self.carrierparam.wire3-self.carrierparam.wire2) - stern=stern:Translate(d23, self:GetFinalBearing(false), true) - end + + -- Landing coordinate + local landingcoord=self:_GetOptLandingCoordinate() -- Distance from stern to aircraft. - local x=unit:GetCoordinate():Get2DDistance(stern) + local x=unit:GetCoordinate():Get2DDistance(landingcoord) -- Altitude of unit corrected by the deck height of the carrier. - local h=unit:GetAltitude()-self.carrierparam.deckheight-4 + local h=self:_GetAltCarrier(unit) -- Glide slope. local glideslope=math.atan(h/x) @@ -6640,18 +7174,11 @@ end -- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. function AIRBOSS:_Lineup(unit, runway) - -- Stern coordinate. - local stern=self:_GetSternCoord() + -- Landing coordinate + local landingcoord=self:_GetOptLandingCoordinate() - -- Ideally we want to land between 2nd and 3rd wire. - if self.carrierparam.wire3 then - -- We take the position of the 3rd wire to approximately account for the length of the aircraft. - local d23=self.carrierparam.wire3 --+0.5*(self.carrierparam.wire3-self.carrierparam.wire2) - stern=stern:Translate(d23, self:GetFinalBearing(false), true) - end - - -- Vector to carrier. - local A=stern:GetVec3() + -- Vector to landing coord. + local A=landingcoord:GetVec3() -- Vector to player. local B=unit:GetVec3() @@ -6701,6 +7228,34 @@ function AIRBOSS:_Lineup(unit, runway) return lineup end +--- Get alitude of aircraft wrt carrier deck. Should give zero when the aircraft touched down. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #number Altitude in meters wrt carrier height. +function AIRBOSS:_GetAltCarrier(unit) + -- Altitude of unit corrected by the deck height of the carrier. + local h=unit:GetAltitude()-self.carrierparam.deckheight-4 + return h +end + +--- Get optimal landing position of the aircraft. Usually between second and third wire. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Optimal landing coordinate. +function AIRBOSS:_GetOptLandingCoordinate() + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + -- Ideally we want to land between 2nd and 3rd wire. + if self.carrierparam.wire3 then + -- We take the position of the 3rd wire to approximately account for the length of the aircraft. + local d23=self.carrierparam.wire3 --+0.5*(self.carrierparam.wire3-self.carrierparam.wire2) + stern=stern:Translate(d23, self:GetFinalBearing(false), true) + end + + return stern +end + --- Get true (or magnetic) heading of carrier. -- @param #AIRBOSS self -- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned. @@ -7111,6 +7666,7 @@ function AIRBOSS:_LSOgrade(playerData) -- No deviations, should be REALLY RARE! grade="_OK_" points=5.0 + G="Unicorn" else if nL>0 then -- Larger deviations ==> "No grade" 2.0 points. @@ -7151,6 +7707,15 @@ function AIRBOSS:_LSOgrade(playerData) G="n/a" end points=1.0 + elseif playerData.fouldeckwo then + if playerData.landed then + --AIRBOSS wants to talk to you! + grade="CUT" + points=0.0 + else + grade="FDWO" + points=-1.0 + end elseif playerData.waveoff then -- Wave Off if playerData.landed then @@ -7191,7 +7756,7 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) -- No flight data ==> return empty string. if fdata==nil then - self:T(self.lid.."Flight data is nil.") + self:T3(self.lid.."Flight data is nil.") return "", 0 end @@ -7299,8 +7864,8 @@ end --- Get short name of the grove step. -- @param #AIRBOSS self +-- @param #string step Player step. -- @param #number n Use -1 for previous or +1 for next. Default 0. --- @param #boolean previous If true return previous step. E.g. -- @return #string Shortcut name "X", "RB", "IM", "AR", "IW". function AIRBOSS:_GS(step, n) local gp @@ -7454,7 +8019,7 @@ function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) -- Debug. local dtext=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..dtext) + self:T(self.lid..dtext) -- Message to player. self:MessageToPlayer(playerData, text, "LSO", nil, 20) @@ -7669,7 +8234,7 @@ function AIRBOSS:_AoACheck(playerData, optaoa) -- Extend or decrease depending on skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then -- Also inform students about optimal value. - hint=hint..string.format(" Optimal AoA is %.1f.", optaoa) + hint=hint..string.format(" Optimal AoA is %.1f.", self:_AoADeg2Units(playerData, optaoa)) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then -- We keep is short normally. elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then @@ -7678,7 +8243,7 @@ function AIRBOSS:_AoACheck(playerData, optaoa) end -- Debriefing text. - local debrief=string.format("AoA %.1f = %d%% deviation from %.1f.", aoa, _error, optaoa) + local debrief=string.format("AoA %.1f = %d%% deviation from %.1f.", self:_AoADeg2Units(playerData, aoa), _error, self:_AoADeg2Units(playerData, optaoa)) return hint, debrief end @@ -7756,7 +8321,9 @@ function AIRBOSS:_Debrief(playerData) local grade, points, analysis=self:_LSOgrade(playerData) -- Insert points to table of all points until player landed. - table.insert(playerData.points, points) + if points and points>=0 then + table.insert(playerData.points, points) + end -- Player has landed and is not airborne any more. local Points=0 @@ -7793,9 +8360,12 @@ function AIRBOSS:_Debrief(playerData) -- LSO grade: (OK) 3.0 PT - LURIM local text=string.format("%s %.1f PT - %s", grade, Points, analysis) + if Points==-1 then + text=string.format("%s n/a PT - Foul deck", grade, Points, analysis) + end -- Wire and Groove time only if not pattern WO. - if not playerData.patternwo then + if not (playerData.patternwo or playerData.fouldeckwo) then -- Wire trapped. Not if pattern WI. if playerData.wire and playerData.wire<=4 then @@ -7821,7 +8391,7 @@ function AIRBOSS:_Debrief(playerData) self:MessageToPlayer(playerData, text, "LSO", "", 30, true) - -- Set step to undefined and check. + -- Set step to undefined and check if other cases apply. playerData.step=AIRBOSS.PatternStep.UNDEFINED @@ -7851,11 +8421,11 @@ function AIRBOSS:_Debrief(playerData) playerData.step=AIRBOSS.PatternStep.INITIAL -- Create a point 3.0 NM astern for re-entry. - local zoneinitial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5), self:GetRadial(2, false, false, false)) + local initial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5), self:GetRadial(2, false, false, false)) -- Get heading and distance to initial zone ~3 NM astern. - heading=playerData.unit:GetCoordinate():HeadingTo(zoneinitial) - distance=playerData.unit:GetCoordinate():Get2DDistance(zoneinitial) + heading=playerData.unit:GetCoordinate():HeadingTo(initial) + distance=playerData.unit:GetCoordinate():Get2DDistance(initial) elseif playerData.case==3 then @@ -7882,6 +8452,15 @@ function AIRBOSS:_Debrief(playerData) self:T2(self.lid..string.format("Player unit not alive!")) end + + elseif playerData.fouldeckwo then + + --------------- + -- Foul Deck -- + --------------- + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER elseif playerData.waveoff then @@ -7891,18 +8470,9 @@ function AIRBOSS:_Debrief(playerData) if playerData.unit:InAir() then - if playerData.case<3 then - - -- Next step: Abeam - playerData.step=AIRBOSS.PatternStep.ABEAM - - else - - -- Next step? Taking Bullseye for now. - playerData.step=AIRBOSS.PatternStep.BULLSEYE - - end - + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + else -- Welcome aboard! @@ -7912,12 +8482,6 @@ function AIRBOSS:_Debrief(playerData) local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 3) - -- Remove player unit from flight and all queues. - self:_RemoveUnitFromFlight(playerData.unit) - - -- Next step undefined. Player landed. - playerData.step=AIRBOSS.PatternStep.UNDEFINED - end elseif playerData.boltered then @@ -7928,26 +8492,11 @@ function AIRBOSS:_Debrief(playerData) if playerData.unit:InAir() then - if playerData.case<3 then - - -- Next step: Abeam - playerData.step=AIRBOSS.PatternStep.ABEAM - - else - - -- Next step? Taking Bullseye for now. - playerData.step=AIRBOSS.PatternStep.BULLSEYE - - end - - else - - -- Next step undefined. Player is not in air any more. - playerData.step=AIRBOSS.PatternStep.UNDEFINED + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER end - - + elseif playerData.landed then ------------ @@ -7956,9 +8505,6 @@ function AIRBOSS:_Debrief(playerData) if not playerData.unit:InAir() then - -- Remove player unit from flight and all queues. - self:_RemoveUnitFromFlight(playerData.unit) - -- Welcome aboard! self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) @@ -7974,6 +8520,12 @@ function AIRBOSS:_Debrief(playerData) end + -- Player landed and is not in air anymore. + if playerData.landed and not playerData.unit:InAir() then + -- TODO: This is not 100% correct if player group has some AI units. But we do it anyway since otherwise player will + self:_RemoveFlightFromQueue(self.Qpattern, playerData) + end + -- Increase number of passes. playerData.passes=playerData.passes+1 @@ -8043,6 +8595,30 @@ end -- MISC functions ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get aircraft nickname. +-- @param #AIRBOSS self +-- @param #string actype Aircraft type name. +-- @return #string Aircraft nickname. E.g. "Hornet" for the F/A-18C or "Tomcat" For the F-14A. +function AIRBOSS:_GetACNickname(actype) + + local nickname="unknown" + if actype==AIRBOSS.AircraftCarrier.A4EC then + nickname="Skyhawk" + elseif actype==AIRBOSS.AircraftCarrier.AV8B then + nickname="Harrier" + elseif actype==AIRBOSS.AircraftCarrier.E2D then + nickname="Hawkeye" + elseif actype==AIRBOSS.AircraftCarrier.F14A_AI or actype==AIRBOSS.AircraftCarrier.F14A or actype==AIRBOSS.AircraftCarrier.F14B then + nickname="Tomcat" + elseif actype==AIRBOSS.AircraftCarrier.FA18C or actype==AIRBOSS.AircraftCarrier.HORNET then + nickname="Hornet" + elseif actype==AIRBOSS.AircraftCarrier.S3B or actype==AIRBOSS.AircraftCarrier.S3BTANKER then + nickname="Viking" + end + + return nickname +end + --- Get onboard number of player or client. -- @param #AIRBOSS self -- @param Wrapper.Group#GROUP group Aircraft group. @@ -8174,9 +8750,14 @@ end -- @return #number Altitude in Anglels = thousands of feet using math.floor(). function AIRBOSS:_GetAngels(alt) - local angels=math.floor(UTILS.MetersToFeet(alt)/1000) - - return angels + if alt then + --local angels=math.floor(UTILS.MetersToFeet(alt)/1000) + local angels=UTILS.Round(UTILS.MetersToFeet(alt)/1000, 0) + return angels + else + return 0 + end + end --- Get unit masses especially fuel from DCS descriptor values. @@ -8559,7 +9140,7 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration elseif receiver==playerData.onboard then -- Sound only to player group. - if sender=="LSO" or sender=="MARSHAL" then + if sender=="LSO" or sender=="MARSHAL" or sender=="AIRBOSS" then self:_Number2Sound(playerData, sender, receiver, delay) end @@ -8698,7 +9279,7 @@ function AIRBOSS:_Number2Sound(playerData, sender, number, delay) local Sender if sender=="LSO" then Sender="LSOCall" - elseif sender=="MARSHAL" then + elseif sender=="MARSHAL" or sender=="AIRBOSS" then Sender="MarshalCall" else self:E(self.lid..string.format("ERROR: Unknown radio sender %s!", tostring(sender))) @@ -8835,12 +9416,14 @@ function AIRBOSS:_AddF10Commands(_unitName) -------------------------------- local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) -- F10/Airboss//F1 Help/F1 Mark Zones - local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) - -- F10/Airboss//F1 Help/F1 Mark Zones/ - missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 - missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 - missionCommands.addCommandForGroup(gid, "Smoke Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F3 - missionCommands.addCommandForGroup(gid, "Flare Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F4 + if self.menumarkzones then + local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) + -- F10/Airboss//F1 Help/F1 Mark Zones/ + missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 + missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 + missionCommands.addCommandForGroup(gid, "Smoke Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F3 + missionCommands.addCommandForGroup(gid, "Flare Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F4 + end -- F10/Airboss//F1 Help/F2 Skill Level local _skillPath=missionCommands.addSubMenuForGroup(gid, "Skill Level", _helpPath) -- F10/Airboss//F1 Help/F2 Skill Level/ @@ -8867,7 +9450,8 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(gid, "Carrier Info", _kneeboardPath, self._DisplayCarrierInfo, self, _unitName) -- F2 missionCommands.addCommandForGroup(gid, "Weather Report", _kneeboardPath, self._DisplayCarrierWeather, self, _unitName) -- F3 missionCommands.addCommandForGroup(gid, "Set Section", _kneeboardPath, self._SetSection, self, _unitName) -- F4 - missionCommands.addCommandForGroup(gid, "Marshal Queue", _kneeboardPath, self._DisplayMarshalQueue, self, _unitName) -- F5 + missionCommands.addCommandForGroup(gid, "Marshal Queue", _kneeboardPath, self._DisplayQueue, self, _unitName, self.Qmarshal, "Marshal") -- F5 + missionCommands.addCommandForGroup(gid, "Pattern Queue", _kneeboardPath, self._DisplayQueue, self, _unitName, self.Qpattern, "Pattern") -- F6 ------------------------- -- F10/Airboss// @@ -8907,14 +9491,14 @@ function AIRBOSS:_ResetPlayerStatus(_unitName) -- Inform player. local text="Status reset executed! You have been removed from all queues." - self:MessageToPlayer(playerData, text, nil, "") + self:MessageToPlayer(playerData, text, "AIRBOSS") -- Remove from marhal stack can collapse stack if necessary. - if self:_InQueue(self.Qmarshal, playerData.group) then - self:_CollapseMarshalStack(playerData, true) - end + --if self:_InQueue(self.Qmarshal, playerData.group) then + -- self:_CollapseMarshalStack(playerData, true) + --end - -- Remove flight from queues. + -- Remove flight from queues. Collapse marshal stack if necessary. self:_RemoveFlight(playerData) -- Initialize player data. @@ -8924,44 +9508,6 @@ function AIRBOSS:_ResetPlayerStatus(_unitName) end end ---- LSO radio check. Will broadcase LSO message at given LSO frequency. --- @param #AIRBOSS self --- @param #string _unitName Name fo the player unit. -function AIRBOSS:_LSORadioCheck(_unitName) - self:F(_unitName) - - -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - - -- Check if we have a unit which is a player. - if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData - if playerData then - -- Broadcase LSO radio check message on LSO radio. - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RADIOCHECK) - end - end -end - ---- Marshal radio check. Will broadcase Marshal message at given Marshal frequency. --- @param #AIRBOSS self --- @param #string _unitName Name fo the player unit. -function AIRBOSS:_MarshalRadioCheck(_unitName) - self:F(_unitName) - - -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - - -- Check if we have a unit which is a player. - if _unit and _playername then - local playerData=self.players[_playername] --#AIRBOSS.PlayerData - if playerData then - -- Broadcase Marshal radio check message on Marshal radio. - self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.RADIOCHECK) - end - end -end - --- Request marshal. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. @@ -9037,6 +9583,8 @@ function AIRBOSS:_RequestCommence(_unitName) local text="" if _unit:IsInZone(self.zoneCCA) then + -- TODO: Check distance to initial or platform. Only allow commence if < max distance. Otherwise say bearing. + if self:_InQueue(self.Qpattern, playerData.group) then -- Flight group is already in pattern queue. @@ -9072,7 +9620,26 @@ function AIRBOSS:_RequestCommence(_unitName) -- TODO: check if recovery window is open. if not self:IsRecovering() then text="Recovery window NOT open yet! However, you are cleared anyway.\n" - end + end + + -- If player is not in the Marshal queue set player case to current case. + if not self:_InQueue(self.Qmarshal, playerData.group) then + + -- Set case. + playerData.case=self.case + + -- Hint about TACAN bearing. + if self.TACANon and playerData.difficulty~=AIRBOSS.Difficulty.HARD then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(playerData.case, true, true, true) + if playerData.case==1 then + -- For case 1 we want the BRC but above routine return FB. + radial=self:GetBRC() + end + text=text..string.format("Select TACAN %03d°, channel %d%s (%s)\n", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + end + + end -- Positive response. if playerData.case==1 then @@ -9085,8 +9652,8 @@ function AIRBOSS:_RequestCommence(_unitName) playerData.step=AIRBOSS.PatternStep.COMMENCING playerData.warning=nil - -- Collaps marshal stack. - self:_CollapseMarshalStack(playerData, false) + -- Remove flight from Marshal queue and collaps stack if necessary. + self:_RemoveFlightFromMarshalQueue(playerData, false) end end @@ -9131,14 +9698,16 @@ function AIRBOSS:_RequestRefueling(_unitName) if self.tanker:IsRunning() or self.tanker:IsRefueling() then -- Get alt of tanker in angels. - local angels=UTILS.Round(UTILS.MetersToFeet(self.tanker.altitude)/1000, 0) + --local angels=UTILS.Round(UTILS.MetersToFeet(self.tanker.altitude)/1000, 0) + local angels=self:_GetAngels(self.tanker.altitude) -- Tanker is up and running. text=string.format("Proceed to tanker at angels %d.", angels) -- State TACAN channel of tanker if defined. if self.tanker.TACANon then - text=text..string.format("\nTanker TACAN channel %d%s (%s)", self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) + text=text..string.format("\nTanker TACAN channel %d%s (%s).", self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) + text=text..string.format("\nRadio frequency %.3f MHz AM.", self.tanker.RadioFreq) end -- Tanker is currently refueling. Inform player. @@ -9147,10 +9716,11 @@ function AIRBOSS:_RequestRefueling(_unitName) end -- Collapse marshal stack if player is in queue. - if self:_InQueue(self.Qmarshal, playerData.group) then - -- TODO: What if only the player and not his section wants to refuel?! - self:_CollapseMarshalStack(playerData, true) - end + self:_RemoveFlightFromMarshalQueue(playerData, true) + -- TODO: What if only the player and not his section wants to refuel?! + --if self:_InQueue(self.Qmarshal, playerData.group) then + -- self:_CollapseMarshalStack(playerData, true) + --end -- Set step to refueling. playerData.step=AIRBOSS.PatternStep.REFUELING @@ -9221,10 +9791,10 @@ function AIRBOSS:_SetSection(_unitName) -- Info on section members. if #playerData.section>0 then text=string.format("Registered flight section:") - text=text..string.format("- %s (lead)", playerData.name) - for _,_flight in paris(playerData.section) do + text=text..string.format("\n- %s (lead)", playerData.name) + for _,_flight in pairs(playerData.section) do local flight=_flight --#AIRBOSS.PlayerData - text=text..string.format("- %s", flight.name) + text=text..string.format("\n- %s", flight.name) flight.seclead=playerData.name -- Inform player that he is now part of a section. @@ -9270,37 +9840,41 @@ function AIRBOSS:_DisplayScoreBoard(_unitName) for _,_grade in pairs(playerData.grades) do local grade=_grade --#AIRBOSS.LSOgrade - -- Add up points - Paverage=Paverage+grade.points - n=n+1 + -- Add up points if >=0. For foul deck WO we give -1 and it does not count. + if grade.points>=0 then + Paverage=Paverage+grade.points + n=n+1 + end end if n>0 then _playerResults[_playerName]=Paverage/n end end - --Sort list! - local _sort=function(a, b) return a>b end - table.sort(_playerResults,_sort) - -- Message text. local text = string.format("Greenie Board (top ten):") - local i=0 - for _playerName,_points in pairs(_playerResults) do - text=text..string.format("\n[%d] %s %.1f||", i+1,_playerName,_points) - - -- Current player data. - local playerData=self.players[_playerName] --#AIRBOSS.PlayerData - - -- Add grades of passes. - for _,_grade in pairs(playerData.grades) do - local grade=_grade --#AIRBOSS.LSOgrade - text=text..string.format("|%.1f", grade.points) - end - - i=i+1 - if i==10 then - break + local i=1 + for _playerName,_points in UTILS.spairs(_playerResults, function(t, a, b) return t[b] < t[a] end) do + + -- Current player data. + local playerData=self.players[_playerName] --#AIRBOSS.PlayerData + + if playerData then + + -- Text. + text=text..string.format("\n[%d] %s %.1f|", i,_playerName,_points) + + -- Add grades of passes. + for _,_grade in pairs(playerData.grades) do + local grade=_grade --#AIRBOSS.LSOgrade + text=text..string.format("|%.1f", grade.points) + end + + i=i+1 + if i>10 then + break + end + end end if i==0 then @@ -9334,32 +9908,36 @@ function AIRBOSS:_DisplayPlayerGrades(_unitName) -- Grades of player: local text=string.format("Your grades, %s:", _playername) - local p=0 + local p=0 -- Average points. + local n=0 -- Number of valid passes. for i,_grade in pairs(playerData.grades) do local grade=_grade --#AIRBOSS.LSOgrade - -- Show final points or points of pass. - local points=grade.finalscore or grade.points + -- Check if points >=0. For foul deck WO we give -1 and pass is not counted. + if grade.points>=0 then - text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, points, grade.details) - - -- Wire trapped if any. - if grade.wire and grade.wire<=4 then - text=text..string.format(" %d-wire", grade.wire) + -- Show final points or points of pass. + local points=grade.finalscore or grade.points + + text=text..string.format("\n[%d] %s %.1f PT - %s", n+1, grade.grade, points, grade.details) + + -- Wire trapped if any. + if grade.wire and grade.wire<=4 then + text=text..string.format(" %d-wire", grade.wire) + end + + -- Time in the groove if any. + if grade.Tgroove and grade.Tgroove<=60 then + text=text..string.format(" Tgroove=%.1f s", grade.Tgroove) + end + + -- Add up points. + p=p+grade.points + n=n+1 end - - -- Time in the groove if any. - if grade.Tgroove and grade.Tgroove<=60 then - text=text..string.format(" Tgroove=%.1f s", grade.Tgroove) - end - - -- Add up points. - p=p+grade.points end - -- Number of grades. - local n=#playerData.grades - + if n>0 then text=text..string.format("\nAverage points = %.1f", p/n) else @@ -9412,58 +9990,16 @@ function AIRBOSS:_DisplayDebriefing(_unitName) end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ --- SKIL LEVEL MENU ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - ---- Set difficulty level. --- @param #AIRBOSS self --- @param #string playername Player name. --- @param #AIRBOSS.Difficulty difficulty Difficulty level. -function AIRBOSS:_SetDifficulty(playername, difficulty) - self:T2({difficulty=difficulty, playername=playername}) - - local playerData=self.players[playername] --#AIRBOSS.PlayerData - - if playerData then - playerData.difficulty=difficulty - local text=string.format("your difficulty level is now: %s.", difficulty) - self:MessageToPlayer(playerData, text, nil, playerData.name, 5) - else - self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) - end -end - ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- KNEEBOARD MENU ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Turn player's aircraft attitude display on or off. +--- Display marshal or pattern queue. -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. -function AIRBOSS:_DisplayAttitude(_unitname) - self:F2(_unitname) - - -- Get player unit and player name. - local unit, playername = self:_GetPlayerUnitAndName(_unitname) - - -- Check if we have a player. - if unit and playername then - - -- Player data. - local playerData=self.players[playername] --#AIRBOSS.PlayerData - - if playerData then - playerData.attitudemonitor=not playerData.attitudemonitor - end - end - -end - ---- Display marshal queue. --- @param #AIRBOSS self --- @param #string _unitname Name of the player unit. -function AIRBOSS:_DisplayMarshalQueue(_unitname) +-- @param #table queue The queue to display. +-- @param #string qname Name of the queue. +function AIRBOSS:_DisplayQueue(_unitname, queue, qname) -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) @@ -9476,17 +10012,37 @@ function AIRBOSS:_DisplayMarshalQueue(_unitname) if playerData then - local text=string.format("Marshal Queue:") - if #self.Qmarshal==0 then + -- Number of group and units in queue + local Nqueue,nqueue=self:_GetQueueInfo(queue, playerData.case) + + local text=string.format("%s Queue:", qname) + if #queue==0 then text=text.." empty" else - for i,_flight in pairs(self.Qmarshal) do - local flight=_flight --#AIRBOSS.FlightGroup - local charlie=self:_GetCharlieTime(flight) - local Charlie=UTILS.SecondsToClock(charlie) - local stack=flight.flag:Get() - text=text..string.format("\n[Stack %d] %s (%s): Case %d, Charlie %s", stack, flight.onboard, flight.actype, flight.case, tostring(Charlie)) + local N=0 + if qname=="Marshal" then + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + local charlie=self:_GetCharlieTime(flight) + local Charlie=UTILS.SecondsToClock(charlie) + local stack=flight.flag:Get() + local angels=self:_GetAngels(self:_GetMarshalAltitude(stack, flight.case)) + local nunit=flight.nunits+#flight.section + local nick=self:_GetACNickname(flight.actype) + N=N+nunit + text=text..string.format("\n[Stack %d] %s (%s*%d): Case %d, Angels %d, Charlie %s", stack, flight.onboard, nick, nunit, flight.case, angels, tostring(Charlie)) + end + elseif qname=="Pattern" then + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + local nunit=flight.nunits+#flight.section + local nick=self:_GetACNickname(flight.actype) + local ptime=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + N=N+nunit + text=text..string.format("\n[%d] %s (%s*%d): Case %d, T=%s", i, flight.onboard, nick, nunit, flight.case, ptime) + end end + text=text..string.format("\nTotal AC: %d (airborne %d)", N, nqueue) end -- Send message. @@ -9562,19 +10118,23 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) -- Recovery tanker TACAN text. local tankertext=nil if self.tanker then - tankertext=string.format("Recovery tanker TACAN ") + tankertext=string.format("Recovery tanker frequency %.3f MHz\n", self.tanker.RadioFreq) if self.tanker.TACANon then - tankertext=tankertext..string.format("%d%s (%s)\n",self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) + tankertext=tankertext..string.format("Recovery tanker TACAN %d%s (%s)",self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) else - tankertext=tankertext.."n/a\n" - end + tankertext=tankertext.."Recovery tanker TACAN n/a" + end end -- Message text. local text=string.format("%s info:\n", self.alias) - text=text..string.format("=============================================\n") + text=text..string.format("================================\n") text=text..string.format("Carrier state %s\n", self:GetState()) - text=text..string.format("Case %d recovery\n", self.case) + if self.case==1 then + text=text..string.format("Case %d recovery ops\n", self.case) + else + text=text..string.format("Case %d recovery ops (%d° offset)\n", self.case, self.holdingoffset) + end text=text..string.format("BRC %03d°\n", self:GetBRC()) text=text..string.format("FB %03d°\n", self:GetFinalBearing(true)) text=text..string.format("Speed %d kts\n", carrierspeed) @@ -9583,7 +10143,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) text=text..string.format("TACAN Channel %s\n", tacan) text=text..string.format("ICLS Channel %s\n", icls) if tankertext then - text=text..tankertext + text=text..tankertext.."\n" end text=text..string.format("# A/C total %d\n", #self.flights) text=text..string.format("# A/C marshal %d (%d)\n", Nmarshal, nmarshal) @@ -9592,7 +10152,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) self:T2(self.lid..text) -- Send message. - self:MessageToPlayer(playerData, text, nil, "", 20, true) + self:MessageToPlayer(playerData, text, nil, "", 30, true) else self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) @@ -9631,8 +10191,8 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) local WD=string.format('%03d°', Wd) local Ts=string.format("%d°C",T) + --[[ 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", UTILS.hPa2mmHg(P)) @@ -9641,10 +10201,15 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) tP=string.format("%.2f inHg", UTILS.hPa2inHg(P)) end + ]] + + local tT=string.format("%d°C",T) + local tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) + local tP=string.format("%.2f inHg", UTILS.hPa2inHg(P)) -- Report text. text=text..string.format("Weather Report at Carrier %s:\n", self.alias) - text=text..string.format("=============================================\n") + 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) @@ -9660,7 +10225,49 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) end end +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- HELP MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Set difficulty level. +-- @param #AIRBOSS self +-- @param #string playername Player name. +-- @param #AIRBOSS.Difficulty difficulty Difficulty level. +function AIRBOSS:_SetDifficulty(playername, difficulty) + self:T2({difficulty=difficulty, playername=playername}) + + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.difficulty=difficulty + local text=string.format("your skill level is now: %s.", difficulty) + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + else + self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + end +end + +--- Turn player's aircraft attitude display on or off. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayAttitude(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.attitudemonitor=not playerData.attitudemonitor + end + end + +end --- Display player status. -- @param #AIRBOSS self @@ -9675,26 +10282,48 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then - - -- Stack and stack altitude. + + -- Pattern step text. + local steptext=playerData.step + if playerData.step==AIRBOSS.PatternStep.HOLDING then + if playerData.holding==nil then + steptext="Transit to Marshal" + elseif playerData.holding==false then + steptext="Marshal (outside zone)" + elseif playerData.holding==true then + steptext="Marshal Stack Holding" + end + end + + -- Stack. local stack=playerData.flag:Get() - local stackalt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) + + -- Stack text. + local stacktext=nil + if stack>0 then + local stackalt=self:_GetMarshalAltitude(stack) + local angels=self:_GetAngels(stackalt) + stacktext=string.format("Marshal Stack %d, Angels %d\n", stack, angels) + end -- Fuel and fuel state. local fuel=playerData.unit:GetFuel()*100 - local fuelstate=self:_GetFuelState(playerData.unit) + local fuelstate=self:_GetFuelState(playerData.unit) -- Player data. local text=string.format("Status of player %s (%s)\n", playerData.name, playerData.callsign) - text=text..string.format("=============================================\n") - text=text..string.format("Current step: %s\n", playerData.step) - text=text..string.format("Skil level: %s\n", playerData.difficulty) - text=text..string.format("Aircraft: %s\n", playerData.actype) - text=text..string.format("Board number: %s\n", playerData.onboard) - text=text..string.format("Fuel state: %.1f lbs/1000 (%.1f %%)\n", fuelstate/1000, fuel) - text=text..string.format("Stack: %d alt=%d ft\n", stack, stackalt) - text=text..string.format("Group: %s\n", playerData.group:GetName()) - text=text..string.format("# units: %d (n=%d)\n", #playerData.group:GetUnits(), playerData.nunits) + text=text..string.format("================================\n") + text=text..string.format("Step: %s\n", steptext) + if stacktext then + text=text..stacktext + end + text=text..string.format("Recovery Case: %d\n", playerData.case) + text=text..string.format("Skill Level: %s\n", playerData.difficulty) + text=text..string.format("Tail # %s (%s)\n", playerData.onboard, self:_GetACNickname(playerData.actype)) + text=text..string.format("Fuel State: %.1f lbs/1000 (%.1f %%)\n", fuelstate/1000, fuel) + --text=text..string.format("Aircraft: %s\n", self:_GetACNickname(playerData.actype)) + --text=text..string.format("Group: %s\n", playerData.group:GetName()) + text=text..string.format("# units: %d (%d/%d)\n", #playerData.group:GetUnits(), playerData.nunits, #playerData.elements) text=text..string.format("Section Lead: %s\n", tostring(playerData.seclead)) text=text..string.format("# section: %d", #playerData.section) for _,_sec in pairs(playerData.section) do @@ -9713,7 +10342,7 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) local brc=self:GetBRC() -- Help player to find its way to the initial zone. - text=text..string.format("\nFly heading %03d° for %.1f NM and turn to BRC %03d°.", flyhdg, flydist, brc) + text=text..string.format("\nFly heading %03d° for %.1f NM and turn to BRC %03d°", flyhdg, flydist, brc) elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then @@ -9728,7 +10357,7 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) local hdg=self:GetRadial(playerData.case, true, true, true) -- Help player to find its way to the initial zone. - text=text..string.format("\nFly heading %03d° for %.1f NM and turn to FB %03d°.", flyhdg, flydist, hdg) + text=text..string.format("\nFly heading %03d° for %.1f NM and turn to %03d°", flyhdg, flydist, hdg) end @@ -9774,7 +10403,7 @@ function AIRBOSS:_MarkMarshalZone(_unitName, flare) zone:FlareZone(FLARECOLOR.White, 45, nil, patternalt) else text="Marking marshal zone with WHITE smoke." - zone:SmokeZone(SMOKECOLOR.Blue, 45, patternalt) + zone:SmokeZone(SMOKECOLOR.White, 45, patternalt) end else @@ -9812,6 +10441,10 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) -- Flare or smoke? if flare then + + ----------- + -- Flare -- + ----------- -- Case I/II: Initial if case==1 or case==2 then @@ -9855,6 +10488,10 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) else + ----------- + -- Smoke -- + ----------- + -- Case I/II: Initial if case==1 or case==2 then text=text.."* initial with GREEN smoke\n" @@ -9904,6 +10541,44 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) end +--- LSO radio check. Will broadcase LSO message at given LSO frequency. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_LSORadioCheck(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then + -- Broadcase LSO radio check message on LSO radio. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RADIOCHECK) + end + end +end + +--- Marshal radio check. Will broadcase Marshal message at given Marshal frequency. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_MarshalRadioCheck(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then + -- Broadcase Marshal radio check message on Marshal radio. + self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.RADIOCHECK) + end + end +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index f677f4c89..00f5fa520 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -35,6 +35,8 @@ -- @field #string TACANmode TACAN mode, i.e. "X" or "Y". Default "Y". Use only "Y" for AA TACAN stations! -- @field #string TACANmorse TACAN morse code. Three letters identifying the TACAN station. Default "TKR". -- @field #boolean TACANon If true, TACAN is automatically activated. If false, TACAN is disabled. +-- @field #number RadioFreq Radio frequency in MHz of the tanker. Default 251 MHz. +-- @field #string RadioModu Radio modulation "AM" or "FM". Default "AM". -- @field #number speed Tanker speed when flying pattern. -- @field #number altitude Tanker orbit pattern altitude. -- @field #number distStern Race-track distance astern. distStern is <0. @@ -153,6 +155,19 @@ -- The mode is *always* "Y" for AA TACAN stations since mode "X" does not work! -- -- In order to completely disable the TACAN beacon, you can use the @{#RECOVERYTANKER.SetTACANoff}() function in your script. +-- +-- ## Radio +-- +-- The radio frequency on optionally modulation can be set via the @{#RECOVERYTANKER.SetRadio}(*frequency*, *modulation*) function. The first parameter denotes the radio frequency the tanker uses in MHz. +-- The second parameter is *optional* and sets the modulation to either AM (default) or FM. +-- +-- For example, +-- +-- TexacoStennis:SetRadio(260) +-- +-- will set the frequency of the tanker to 260 MHz AM. +-- +-- **Note** that if this is not set, the tanker frequency will be automatically set to **251 MHz AM**. -- -- ## Pattern Update -- @@ -217,6 +232,8 @@ RECOVERYTANKER = { TACANmode = nil, TACANmorse = nil, TACANon = nil, + RadioFreq = nil, + RadioModu = nil, altitude = nil, speed = nil, distStern = nil, @@ -237,7 +254,7 @@ RECOVERYTANKER = { --- Class version. -- @field #string version -RECOVERYTANKER.version="1.0.1" +RECOVERYTANKER.version="1.0.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -297,6 +314,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetLowFuelThreshold() self:SetRespawnOnOff() self:SetTACAN() + self:SetRadio() self:SetPatternUpdateDistance() self:SetPatternUpdateHeading() self:SetPatternUpdateInterval() @@ -623,6 +641,17 @@ function RECOVERYTANKER:SetTACAN(channel, morse) return self end +--- Set radio frequency and optionally modulation of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number frequency Radio frequency in MHz. Default 251 MHz. +-- @param #string modulation Radio modulation, either "AM" or "FM". Default "AM". +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRadio(frequency, modulation) + self.RadioFreq=frequency or 251 + self.RadioModu=modulation or "AM" + return self +end + --- Activate debug mode. Marks of pattern on F10 map and debug messages displayed on screen. -- @param #RECOVERYTANKER self -- @return #RECOVERYTANKER self @@ -692,6 +721,11 @@ function RECOVERYTANKER:onafterStart(From, Event, To) -- Spawn tanker. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. local Spawn=SPAWN:NewWithAlias(self.tankergroupname, tankergroupalias) + -- Set radio frequency and modulation. + Spawn:InitRadioCommsOnOff(true) + Spawn:InitRadioFrequency(self.RadioFreq) + Spawn:InitRadioModulation(self.RadioModu) + -- Spawn on carrier. if self.takeoff==SPAWN.Takeoff.Air then @@ -789,8 +823,15 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) self:T(self.lid..text) - -- Respawn tanker. + -- Set heading for respawn template. self.tanker:InitHeading(self.tanker:GetHeading()) + + -- Set radio for respawn template. + self.tanker:InitRadioCommsOnOff(true) + self.tanker:InitRadioFrequency(self.RadioFreq) + self.tanker:InitRadioModulation(self.RadioModu) + + -- Respawn tanker. self.tanker=self.tanker:Respawn(nil, true) -- Create tanker beacon and activate TACAN. @@ -963,9 +1004,13 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) local text=string.format("Respawning recovery tanker group %s.", group:GetName()) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) self:T(self.lid..text) + + -- Set radio for respawn template. + group:InitRadioCommsOnOff(true) + group:InitRadioFrequency(self.RadioFreq) + group:InitRadioModulation(self.RadioModu) -- Respawn tanker. - --self.tanker=group:RespawnAtCurrentAirbase() -- Delaying respawn due to DCS bug https://github.com/FlightControl-Master/MOOSE/issues/1076 SCHEDULER:New(nil , group.RespawnAtCurrentAirbase, {group}, 1) diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 04fd9e93f..2f5803e85 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -219,50 +219,62 @@ AIRBASE.Normandy = { --- These are all airbases of the Persion Gulf Map: -- --- * AIRBASE.PersianGulf.Fujairah_Intl --- * AIRBASE.PersianGulf.Qeshm_Island --- * AIRBASE.PersianGulf.Sir_Abu_Nuayr -- * AIRBASE.PersianGulf.Abu_Musa_Island_Airport -- * AIRBASE.PersianGulf.Bandar_Abbas_Intl -- * AIRBASE.PersianGulf.Bandar_Lengeh --- * AIRBASE.PersianGulf.Tunb_Island_AFB --- * AIRBASE.PersianGulf.Havadarya --- * AIRBASE.PersianGulf.Lar_Airbase --- * AIRBASE.PersianGulf.Sirri_Island --- * AIRBASE.PersianGulf.Tunb_Kochak -- * AIRBASE.PersianGulf.Al_Dhafra_AB -- * AIRBASE.PersianGulf.Dubai_Intl -- * AIRBASE.PersianGulf.Al_Maktoum_Intl +-- * AIRBASE.PersianGulf.Fujairah_Intl +-- * AIRBASE.PersianGulf.Tunb_Island_AFB +-- * AIRBASE.PersianGulf.Havadarya -- * AIRBASE.PersianGulf.Khasab +-- * AIRBASE.PersianGulf.Lar_Airbase -- * AIRBASE.PersianGulf.Al_Minhad_AB +-- * AIRBASE.PersianGulf.Qeshm_Island -- * AIRBASE.PersianGulf.Sharjah_Intl --- * AIRBASE.PersianGulf.Shiraz_International_Airport +-- * AIRBASE.PersianGulf.Sirri_Island +-- * AIRBASE.PersianGulf.Tunb_Kochak +-- * AIRBASE.PersianGulf.Sir_Abu_Nuayr -- * AIRBASE.PersianGulf.Kerman_Airport --- * AIRBASE.PersianGulf.Jiroft_Airport +-- * AIRBASE.PersianGulf.Shiraz_International_Airport +-- * AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport +-- * AIRBASE.PersianGulf.Bandar-e-Jask_airfield +-- * AIRBASE.PersianGulf.Abu_Dhabi_International_Airport +-- * AIRBASE.PersianGulf.Al-Bateen_Airport +-- * AIRBASE.PersianGulf.Kish_International_Airport +-- * AIRBASE.PersianGulf.Al_Ain_International_Airport -- * AIRBASE.PersianGulf.Lavan_Island_Airport +-- * AIRBASE.PersianGulf.Jiroft_Airport -- @field PersianGulf AIRBASE.PersianGulf = { - ["Fujairah_Intl"] = "Fujairah Intl", - ["Qeshm_Island"] = "Qeshm Island", - ["Sir_Abu_Nuayr"] = "Sir Abu Nuayr", ["Abu_Musa_Island_Airport"] = "Abu Musa Island Airport", ["Bandar_Abbas_Intl"] = "Bandar Abbas Intl", ["Bandar_Lengeh"] = "Bandar Lengeh", + ["Al_Dhafra_AB"] = "Al Dhafra AB", + ["Dubai_Intl"] = "Dubai Intl", + ["Al_Maktoum_Intl"] = "Al Maktoum Intl", + ["Fujairah_Intl"] = "Fujairah Intl", ["Tunb_Island_AFB"] = "Tunb Island AFB", ["Havadarya"] = "Havadarya", + ["Khasab"] = "Khasab", ["Lar_Airbase"] = "Lar Airbase", + ["Al_Minhad_AB"] = "Al Minhad AB", + ["Qeshm_Island"] = "Qeshm Island", + ["Sharjah_Intl"] = "Sharjah Intl", ["Sirri_Island"] = "Sirri Island", ["Tunb_Kochak"] = "Tunb Kochak", - ["Al_Dhafra_AB"] = "Al Dhafra AB", - ["Dubai_Intl"] = "Dubai Intl", - ["Al_Maktoum_Intl"] = "Al Maktoum Intl", - ["Khasab"] = "Khasab", - ["Al_Minhad_AB"] = "Al Minhad AB", - ["Sharjah_Intl"] = "Sharjah Intl", - ["Shiraz_International_Airport"] = "Shiraz International Airport", + ["Sir_Abu_Nuayr"] = "Sir Abu Nuayr", ["Kerman_Airport"] = "Kerman Airport", - ["Jiroft_Airport"] = "Jiroft Airport", + ["Shiraz_International_Airport"] = "Shiraz International Airport", + ["Sas_Al_Nakheel_Airport"] = "Sas Al Nakheel Airport", + ["Bandar-e-Jask_airfield"] = "Bandar-e-Jask airfield", + ["Abu_Dhabi_International_Airport"] = "Abu Dhabi International Airport", + ["Al-Bateen_Airport"] = "Al-Bateen Airport", + ["Kish_International_Airport"] = "Kish International Airport", + ["Al_Ain_International_Airport"] = "Al Ain International Airport", ["Lavan_Island_Airport"] = "Lavan Island Airport", + ["Jiroft_Airport"] = "Jiroft Airport", } --- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 81b203ca9..f812716dc 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1455,6 +1455,41 @@ function GROUP:InitRandomizePositionRadius( OuterRadius, InnerRadius ) return self end +--- Sets the radio comms on or off when the group is respawned. Same as checking/unchecking the COMM box in the mission editor. +-- @param #GROUP self +-- @param #number switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group. +-- @return #GROUP self +function GROUP:InitRadioCommsOnOff(switch) + self:F({switch=switch} ) + self.InitRespawnRadio=switch or true +end + +--- Sets the radio frequency of the group when it is respawned. +-- @param #GROUP self +-- @param #number frequency The frequency in MHz. +-- @return #GROUP self +function GROUP:InitRadioFrequency(frequency) + self:F({frequency=frequency} ) + + self.InitRespawnFreq=frequency + + return self +end + +--- Set radio modulation when the group is respawned. Default is AM. +-- @param #GROUP self +-- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM. +-- @return #GROUP self +function GROUP:InitRadioModulation(modulation) + self:F({modulation=modulation}) + if modulation and modulation:lower()=="fm" then + self.InitRespawnModu=radio.modulation.FM + else + self.InitRespawnModu=radio.modulation.AM + end + return self +end + --- Respawn the @{Wrapper.Group} at a @{Point}. -- The method will setup the new group template according the Init(Respawn) settings provided for the group. @@ -1477,7 +1512,7 @@ end -- -- @param Wrapper.Group#GROUP self -- @param #table Template (optional) The template of the Group retrieved with GROUP:GetTemplate(). If the template is not provided, the template will be retrieved of the group itself. --- @param #boolean Reset Reset positons if TRUE. +-- @param #boolean Reset Reset positions if TRUE. -- @return Wrapper.Group#GROUP self function GROUP:Respawn( Template, Reset ) @@ -1606,6 +1641,17 @@ function GROUP:Respawn( Template, Reset ) end + -- Set radio frequency and modulation. + if self.InitRespawnRadio then + Template.communication=self.InitRespawnRadio + end + if self.InitRespawnFreq then + Template.frequency=self.InitRespawnFreq + end + if self.InitRespawnModu then + Template.modulation=self.InitRespawnModu + end + -- Destroy old group. Dont trigger any dead/crash events since this is a respawn. self:Destroy(false) @@ -1714,12 +1760,23 @@ function GROUP:RespawnAtCurrentAirbase(SpawnTemplate, Takeoff, Uncontrolled) -- -- Set uncontrolled state. SpawnTemplate.uncontrolled=Uncontrolled + + -- Set radio frequency and modulation. + if self.InitRespawnRadio then + SpawnTemplate.communication=self.InitRespawnRadio + end + if self.InitRespawnFreq then + SpawnTemplate.frequency=self.InitRespawnFreq + end + if self.InitRespawnModu then + SpawnTemplate.modulation=self.InitRespawnModu + end -- Destroy old group. self:Destroy(false) -- Spawn new group. - _DATABASE:Spawn( SpawnTemplate ) + _DATABASE:Spawn(SpawnTemplate) -- Reset events. self:ResetEvents() From 6b85141a39d881d28c5f6fc6d97db47a3c87cce8 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 13 Jan 2019 00:23:27 +0100 Subject: [PATCH 134/147] AIRBOSS v0.8.1 * Persistence * Fixed menu mark zones bug * minor things --- .../Moose/Functional/Warehouse.lua | 2 +- Moose Development/Moose/Ops/Airboss.lua | 764 +++++++++++++----- 2 files changed, 579 insertions(+), 187 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index e95ccb6ec..ed4b8c31f 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -2449,7 +2449,7 @@ function WAREHOUSE:SetAutoDefenceOff() return self end ---- Set auto defence off. This is the default. +--- Enable auto save of warehouse assets at mission end event. -- @param #WAREHOUSE self -- @param #string path Path where to save the asset data file. -- @param #string filename File name. Default is generated automatically from warehouse id. diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index aac5669d2..51e0612b9 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -6,19 +6,19 @@ -- -- * CASE I, II and III recoveries. -- * Supports human pilots as well as AI flight groups. --- * Automatic LSO grading (WIP). --- * Different skill levels from on-the-fly tips for flight students to ziplip for pros. --- * Define recovery time windows with individual recovery cases. +-- * Automatic LSO grading (WIP) including (optional) live grading while in the groove. +-- * Different skill levels from on-the-fly tips for flight students to *ziplip* for pros. +-- * Define recovery time windows with individual recovery cases in the same mission. -- * Automatic TACAN and ICLS channel setting of carrier. -- * Separate radio channels for LSO and Marshal transmissions. -- * Voice over support for LSO and Marshal radio transmissions. --- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, --- help function (player aircraft attitude, marking of pattern zones etc). +-- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, help function (aircraft attitude, marking of zones etc). -- * Recovery tanker and refueling option via integration of @{Ops.RecoveryTanker} class. -- * Rescue helicopter option via @{Ops.RescueHelo} class. --- * Many parameters customizable by convenient user API functions. +-- * Many parameters customizable by convenient user API functions. -- * Multiple carrier support due to object oriented approach. -- * Unlimited number of players. +-- * Persistence of player results (optional). LSO grading data is saved to csv file. -- * Finite State Machine (FSM) implementation. -- -- **Supported Carriers:** @@ -45,7 +45,6 @@ -- -- ### Some Open Questions? -- --- * What are the conditions for a foul deck wave off? -- * What is the next step after a pattern wave off during Case II or III recovery? -- * What is the condition for a "fly through" (\\ or /) LSO grade? -- * The above question is one of many regarding LSO grade. If you have more info, please share. @@ -133,6 +132,11 @@ -- @field #number dTqueue Time interval in seconds for updating the queues etc. -- @field #number dTstatus Time interval for call FSM status updates. -- @field #boolean menumarkzones If false, disables the option to mark zones via smoke or flares. +-- @field #boolean menusmokezones If false, disables the option to mark zones via smoke. +-- @field #table playerscores Table holding all player scores and grades. +-- @field #boolean autosave If true, all player grades are automatically saved to a file on disk. +-- @field #string autosavepath Path where the player grades file is saved on auto save. +-- @field #string autosavefilename File name of the auto player grades save file. Default is auto generated from carrier name/alias. -- @extends Core.Fsm#FSM --- Be the boss! @@ -504,7 +508,7 @@ -- A foul deck waveoff is called by the LSO if an aircraft is detected within the landing area when an approaching aircraft is crossing the ship's wake during Case I/II operations, -- or with an aircraft approaching the 3/4 NM during Case III operations. -- --- The approaching aircraft will be notified via radio comms an is supposed to overfly the landing area and enter the Bolter pattern. **The pass is not graded**. +-- The approaching aircraft will be notified via LSO radio comms and is supposed to overfly the landing area to enter the Bolter pattern. **This pass is not graded**. -- -- # AI Handling -- @@ -533,6 +537,95 @@ -- Case I recovery. However, I don't think the AI can do a proper Case III recovery. If you give the AI the landing command, it is out of our hands and will -- always go for a Case I in the final pattern part. Maybe this will improve in future DCS version but right now, there is not much we can do about it. -- +-- # Persistence of Player Results +-- +-- LSO grades of players can be saved to disk and later reloaded when a new mission is started. +-- +-- ## Prerequisites +-- +-- **Important** By default, DCS does not allow for writing data to files. Therefore, one first has to comment out the line "sanitizeModule('io')" and "sanitizeModule('lfs')", i.e. +-- +-- do +-- sanitizeModule('os') +-- --sanitizeModule('io') -- required for saving files +-- --sanitizeModule('lfs') -- optional for setting the default path to your "Saved Games\DCS" folder +-- require = nil +-- loadlib = nil +-- end +-- +-- in the file "MissionScripting.lua", which is located in the subdirectory "Scripts" of your DCS installation root directory. +-- +-- ** WARNING ** Desanitizing the "io" and "lfs" modules makes your machine or server vunarable to attacks from the outside! Use this at your own risk. +-- +-- ## Save Results +-- +-- Saving asset data to file is achieved by the @{AIRBOSS.Save}(*path*, *filename*) function. +-- +-- The parameter *path* specifies the path on the file system where the +-- player grades are saved. If you do not specify a path, the file is saved your the DCS installation root directory if the **lfs** module is *not* desanizied or +-- your "Saved Games\\DCS" folder in case you did desanitize the **lfs** module. +-- +-- The parameter *filename* is optional and defines the name of the saved file. By default this is automatically created from the AIRBOSS carrier name/alias, i.e. +-- "Airboss-USS Stennis_LSOgrades.csv", if the alias is "USS Stennis". +-- +-- In the easiest case, you desanitize the **io** and **lfs** modules and just add the line +-- +-- airbossStennis:Save() +-- +-- If you want to specify an explicit path you can do this by +-- +-- airbossStennis:Save("D:\\My Airboss Data\\") +-- +-- This will save all player grades to in "D:\\My Airboss Data\\Airboss-USS Stennis_LSOgrades.csv". +-- +-- ### Automatic Saving +-- +-- The player grades can be saved automatically after each graded player pass via the @{AIRBOSS.SetAutoSave}(*path*, *filename*) function. Again the parameters *path* and *filename* are optional. +-- In the simplest case, you desanitize the **lfs** module and just add +-- +-- +-- airbossStennis:SetAutoSave() +-- +-- Note that the the stats are saved after the *final* grade has been given, i.e. the player has landed on the carrier. After intermediate results such as bolters or waveoffs the stats are not automatically saved. +-- +-- In case you want to specify an explicit path, you can write +-- +-- airbossStennis:SetAutoSave("D:\\My Airboss Data\\") +-- +-- ## Results Output +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_PersistenceResultsTable.png) +-- +-- The results file is stored as comma separated file. The columns are +-- * *Name*: The player name. +-- * *Pass*: A running number counting the passes of the player +-- * *Points Final*: The final points (i.e. when the player has landed). This is the average over all previous bolters or waveoffs, if any. +-- * *Points Pass*: The points of each pass including bolters and waveoffs. +-- * *Grade*: LSO grade. +-- * *Details*: Detailed analysis of deviations within the groove. +-- * *Wire*: Trapped wire, if any. +-- * *Tgroove*: Time in the groove in seconds (not applicable during Case III). +-- * *Case*: The recovery case operations in progress during the pass. +-- +-- ## Load Results +-- +-- Loading player grades from file is achieved by the @{AIRBOSS.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the +-- data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory. +-- The parameter *filename* is optional and defines the name of the file to load. By default this is automatically generated from the AIBOSS carrier name/alias, for example +-- "Airboss-USS Stennis_LSOgrades.csv". +-- +-- Note that the AIRBOSS FSM **must not be started** in order to load the data. In other words, loading should happen **after** the +-- @{#AIRBOSS.New} command is specified in the code but **before** the @{#AIRBOSS.Start} command is given. +-- +-- Loading the player results is done by +-- +-- airbossStennis:New("USS Stennis") +-- airbossStennis:Load("D:\\My Airboss Data\\") +-- -- Additional specification of parameters such as recovery windows etc, if required. +-- airbossStennis:Start() +-- +-- This sequence loads all available player grades from file. +-- -- # Debugging -- -- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in @@ -622,6 +715,11 @@ AIRBOSS = { dTqueue = nil, dTstatus = nil, menumarkzones = nil, + menusmokezones = nil, + playerscores = nil, + autosave = nil, + autosavefile = nil, + autosavepath = nil, } --- Player aircraft types capable of landing on carriers. @@ -1145,9 +1243,11 @@ AIRBOSS.Difficulty={ -- @type AIRBOSS.LSOgrade -- @field #string grade LSO grade, i.e. _OK_, OK, (OK), --, CUT -- @field #number points Points received. +-- @field #number finalscore Points received after player has finally landed. This is the average over all incomplete passes (bolter, waveoff) before. -- @field #string details Detailed flight analysis. -- @field #number wire Wire caught. -- @field #number Tgroove Time in the groove in seconds. +-- @field #number case Recovery case. --- Checkpoint parameters triggering the next step in the pattern. -- @type AIRBOSS.Checkpoint @@ -1202,7 +1302,6 @@ AIRBOSS.Difficulty={ -- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. -- @field #table debrief Debrief analysis of the current step of this pass. -- @field #table lastdebrief Debrief of player performance of last completed pass. --- @field #table grades LSO grades of player passes. -- @field #boolean landed If true, player landed or attempted to land. -- @field #boolean boltered If true, player boltered. -- @field #boolean waveoff If true, player was waved off during final approach. @@ -1224,21 +1323,22 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.8.0" +AIRBOSS.version="0.8.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Allow up to two flights per Case I marshal stack. +-- TODO: Add max stack for Case I and define waiting queue outside CCZ. +-- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! -- TODO: Maybe do an additional step at the initial (Case II) or bullseye (Case III) and register player in case he missed some steps. --- TODO: What happens when section lead or member dies. --- TODO: Include recovery tanker into next stack calculation. Angels six should be empty. +-- TODO: What happens when section lead or member dies? -- TODO: Player eject and crash debrief "gradings". -- TODO: Subtitles off options on player level. -- TODO: PWO during case 2/3. Also when too close to other player. -- TODO: Option to filter AI groups for recovery. --- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! --- TODO: Persistence of results. +-- DONE: Persistence of results. -- DONE: Foul deck waveoff. -- DONE: Get Charlie time estimate function. -- DONE: Average player grades until landing. @@ -1323,6 +1423,9 @@ function AIRBOSS:New(carriername, alias) -- Create carrier beacon. self.beacon=BEACON:New(self.carrier) + + -- Init player scores table. + self.playerscores={} ------------- --- Defaults: @@ -1379,7 +1482,8 @@ function AIRBOSS:New(carriername, alias) self:SetStatusUpdateTime() -- Menu options - self:SetMenuMarkZones(false) + self:SetMenuMarkZones() + self:SetMenuSmokeZones() -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then @@ -1532,12 +1636,14 @@ function AIRBOSS:New(carriername, alias) -- Add FSM transitions. -- From State --> Event --> To State + self:AddTransition("Stopped", "Load", "Stopped") -- Load player scores from file. self:AddTransition("Stopped", "Start", "Idle") -- Start AIRBOSS script. self:AddTransition("*", "Idle", "Idle") -- Carrier is idling. self:AddTransition("Idle", "RecoveryStart", "Recovering") -- Start recovering aircraft. self:AddTransition("Recovering", "RecoveryStop", "Idle") -- Stop recovering aircraft. self:AddTransition("*", "Status", "*") -- Update status of players and queues. self:AddTransition("*", "RecoveryCase", "*") -- Switch to another case recovery. + self:AddTransition("*", "Save", "*") -- Save player scores to file. self:AddTransition("*", "Stop", "Stopped") -- Stop AIRBOSS FMS. @@ -1599,6 +1705,52 @@ function AIRBOSS:New(carriername, alias) -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + --- Triggers the FSM event "Save" that saved the player scores to a file. + -- @function [parent=#AIRBOSS] Save + -- @param #AIRBOSS self + -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + + --- Triggers the FSM delayed event "Save" that saved the player scores to a file. + -- @function [parent=#AIRBOSS] __Save + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + + --- On after "Save" event user function. Called when the player scores are saved to disk. + -- @function [parent=#AIRBOSS] OnAfterSave + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + + + --- Triggers the FSM event "Load" that loads the player scores from a file. AIRBOSS FSM must **not** be started at this point. + -- @function [parent=#AIRBOSS] Load + -- @param #AIRBOSS self + -- @param #string path Path where the file is located. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + + --- Triggers the FSM delayed event "Load" that loads the player scores from a file. AIRBOSS FSM must **not** be started at this point. + -- @function [parent=#AIRBOSS] __Load + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #string path Path where the file is located. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + + --- On after "Load" event user function. Called when the player scores are loaded from disk. + -- @function [parent=#AIRBOSS] OnAfterLoad + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is located. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + + --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. -- @function [parent=#AIRBOSS] Stop -- @param #AIRBOSS self @@ -1760,7 +1912,23 @@ end -- @param #boolean switch If true or nil, menu is enabled. If false, menu is not available to players. -- @return #AIRBOSS self function AIRBOSS:SetMenuMarkZones(switch) - self.menumarkzones=switch or true + if switch==nil or switch==true then + self.menumarkzones=true + else + self.menumarkzones=false + end +end + +--- Enable or disable F10 radio menu for marking zones via smoke. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, menu is enabled. If false, menu is not available to players. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuSmokeZones(switch) + if switch==nil or switch==true then + self.menusmokezones=true + else + self.menusmokezones=false + end end --- Set TACAN channel of carrier. @@ -1871,7 +2039,7 @@ end --- Do not handle AI aircraft. -- @param #AIRBOSS self --- @return #ARIBOSS self +-- @return #AIRBOSS self function AIRBOSS:SetHandleAIOFF() self.handleai=false return self @@ -1881,7 +2049,7 @@ end --- Define recovery tanker associated with the carrier. -- @param #AIRBOSS self -- @param Ops.RecoveryTanker#RECOVERYTANKER recoverytanker Recovery tanker object. --- @return #ARIBOSS self +-- @return #AIRBOSS self function AIRBOSS:SetRecoveryTanker(recoverytanker) self.tanker=recoverytanker return self @@ -1890,7 +2058,7 @@ end --- Define warehouse associated with the carrier. -- @param #AIRBOSS self -- @param Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. --- @return #ARIBOSS self +-- @return #AIRBOSS self function AIRBOSS:SetWarehouse(warehouse) self.warehouse=warehouse return self @@ -1903,7 +2071,7 @@ end -- * "TOPGUN Graduate" = @{#AIRBOSS.Difficulty.Hard} -- @param #AIRBOSS self -- @param #string skill Player skill. Default "Naval Aviator". --- @return #ARIBOSS self +-- @return #AIRBOSS self function AIRBOSS:SetDefaultPlayerSkill(skill) -- Set skill or normal. @@ -1926,6 +2094,18 @@ function AIRBOSS:SetDefaultPlayerSkill(skill) return self end +--- Enable auto save of player results each time a player is *finally* graded. *Finally* means after the player landed on the carrier! After intermediate passes (bolter or waveoff) the stats are *not* saved. +-- @param #AIRBOSS self +-- @param #string path Path where to save the asset data file. Default is the DCS root installation directory or your "Saved Games\\DCS" folder if lfs was desanitized. +-- @param #string filename File name. Default is generated automatically from airboss carrier name/alias. +-- @return #AIRBOSS self +function AIRBOSS:SetAutoSave(path, filename) + self.autosave=true + self.autosavepath=path + self.autosavefile=filename + return self +end + --- Activate debug mode. Display debug messages on screen. -- @param #AIRBOSS self -- @return #AIRBOSS self @@ -2043,7 +2223,8 @@ function AIRBOSS:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Land) self:HandleEvent(EVENTS.Crash) self:HandleEvent(EVENTS.Ejection) - self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) + --self:HandleEvent(EVENTS.MissionEnd) -- Start status check in 1 second. self:__Status(1) @@ -2609,48 +2790,6 @@ function AIRBOSS:_GetETAatNextWP() return eta end - ---- Estimated the carrier position at some point in the future given the current waypoints and speeds. --- @param #AIRBOSS self --- @param #number time Absolute mission time at which the carrier position is requested. --- @return Core.Point#COORDINATE Coordinate of the carrier at the given time. -function AIRBOSS:_GetCarrierFuture(time) - - local nwp=self.currentwp - - local waypoints={} - local lastwp=nil --Core.Point#COORDINATE - for i=1,#self.waypoints do - - if i>nwp then - table.insert(waypoints, self.waypoints[i]) - elseif i==nwp then - lastwp=self.waypoints[i] - end - - end - - -- Current abs. time. - local tnow=timer.getAbsTime() - - local p=self:GetCoordinate() - local v=self.carrier:GetVelocityMPS() - - local s=p:Get2DDistance(self.waypoints[nwp+1]) - - -- v=s/t <==> t=s/v - local t=s/v - - local eta=UTILS.SecondsToClock(t+tnow) - - - for _,_wp in ipairs(waypoints) do - local wp=_wp --Core.Point#COORDINATE - - end - -end - --- Init parameters for USS Stennis carrier. -- @param #AIRBOSS self function AIRBOSS:_InitStennis() @@ -3998,7 +4137,7 @@ function AIRBOSS:_GetQueueInfo(queue, case) local n=countunitsinair(flight.group) if n>0 then ngroup=ngroup+1 - nunits=nunits+n + nunits=nunits+n end --TODO: add section members? @@ -4172,9 +4311,6 @@ function AIRBOSS:_NewPlayer(unitname) -- Number of passes done by player in this slot. playerData.passes=0 --playerData.passes or 0 - - -- LSO grades. - playerData.grades=playerData.grades or {} -- Debriefing tables. playerData.lastdebrief=playerData.lastdebrief or {} @@ -4194,6 +4330,9 @@ function AIRBOSS:_NewPlayer(unitname) -- Init player data. self.players[playername]=playerData + -- Init player grades table if necessary. + self.playerscores[playername]=self.playerscores[playername] or {} + -- Welcome player message. self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), "AIRBOSS", "", 5) @@ -4740,8 +4879,8 @@ function AIRBOSS:_CheckPlayerStatus() elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then - -- Debriefing in 10 seconds. - SCHEDULER:New(nil, self._Debrief, {self, playerData}, 10) + -- Debriefing in 6 seconds. + SCHEDULER:New(nil, self._Debrief, {self, playerData}, 6) -- Undefined status. playerData.step=AIRBOSS.PatternStep.UNDEFINED @@ -4805,17 +4944,9 @@ function AIRBOSS:OnEventBirth(EventData) -- Add Menu commands. self:_AddF10Commands(_unitName) - -- Init new player data. - --local playerData=self:_NewPlayer(_unitName) - -- Delaying the new player for a second, because AI units of the flight would not be registered correctly. SCHEDULER:New(nil, self._NewPlayer, {self, _unitName}, 1) - -- Init player data. - --self.players[_playername]=playerData - - -- Welcome player message. - --self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), "AIRBOSS", "", 5) end end @@ -5015,6 +5146,14 @@ function AIRBOSS:OnEventCrash(EventData) self:_RemoveFlight(flight, true) end + -- Remove all grades until a final grade is reached. + local grades=self.playerscores[_playername] + if grades and #grades>0 then + while #grades>0 and grades[#grades].finalscore==nil do + table.remove(grades, #grades) + end + end + else -- Debug message. self:T2(self.lid..string.format("AI unit %s crashed!", EventData.IniUnitName)) @@ -5047,8 +5186,17 @@ function AIRBOSS:OnEventEjection(EventData) if flight then self:_RemoveFlight(flight, true) end + + -- Remove all grades until a final grade is reached. + local grades=self.playerscores[_playername] + if grades and #grades>0 then + while #grades>0 and grades[#grades].finalscore==nil do + table.remove(grades, #grades) + end + end + else - -- + -- Debug message. self:T2(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) -- Remove unit from flight and queues. @@ -5082,74 +5230,33 @@ function AIRBOSS:_PlayerLeft(EventData) -- Remove flight completely from all queues and collapse marshal if necessary. if flight then self:_RemoveFlight(flight, true) + end + + -- Remove all grades until a final grade is reached. + local grades=self.playerscores[_playername] + if grades and #grades>0 then + while #grades>0 and grades[#grades].finalscore==nil do + table.remove(grades, #grades) + end end end end ---- General event handler. +--[[ +--- Airboss event function handling the mission end event. +-- Handles the case when the mission is ended. -- @param #AIRBOSS self --- @param #table Event DCS event table. -function AIRBOSS:onEvent(Event) - self:F3(Event) +-- @param Core.Event#EVENTDATA EventData Event data. +function AIRBOSS:OnEventMissionEnd(EventData) - if Event == nil or Event.initiator == nil then - self:T3(AIRBOSS.lid.."Skipping onEvent. Event or Event.initiator unknown.") - return true + -- Auto save player results. + if self.autosave then + self:Save(self.autosavepath, self.autosavefile) end - if Unit.getByName(Event.initiator:getName()) == nil then - self:T3(AIRBOSS.lid.."Skipping onEvent. Initiator unit name unknown.") - return true - end - - local DCSiniunit = Event.initiator - - local EventData={} --Core.Event#EVENTDATA - local _playerunit=nil - local _playername=nil - - if Event.initiator then - EventData.IniUnitName = Event.initiator:getName() - EventData.IniDCSGroup = Event.initiator:getGroup() - EventData.IniGroupName = Event.initiator:getGroup():getName() - EventData.IniUnit = UNIT:Find(DCSiniunit) - EventData.IniDCSUnit = Event.initiator - -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. - _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName) - end - - -- Event info. - self:T3(self.lid..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) - self:T3(self.lid..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) - self:T3(self.lid..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) - self:T3(self.lid..string.format("EVENT: Ini player = %s" , tostring(_playername))) - - -- Call event PlayerLeaveUnit function. - if Event.id==world.event.S_EVENT_PLAYER_LEAVE_UNIT and _playername then - --self:OnEventPlayerLeaveUnit(EventData) - self:_PlayerLeft(EventData) - end - - --[[ - - -- Call event Birth function. - if Event.id==world.event.S_EVENT_BIRTH and _playername then - self:OnEventBirth(EventData) - end - - -- Call event Ejection function. - if Event.id==world.event.S_EVENT_EJECTION and _playername then - self:OnEventEjection(EventData) - end - - -- Call event Crash function. - if Event.id==world.event.S_EVENT_CRASH and _playername then - self:OnEventCrash(EventData) - end - - ]] end +]] ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- PATTERN functions @@ -8338,7 +8445,7 @@ function AIRBOSS:_Debrief(playerData) Points=Points/#playerData.points -- Reset points array. - playerData.points={} + playerData.points={} else -- Player boltered or was waved off ==> We display the normal points. Points=points @@ -8354,9 +8461,10 @@ function AIRBOSS:_Debrief(playerData) if playerData.landed and not playerData.unit:InAir() then mygrade.finalscore=Points end + mygrade.case=playerData.case - -- Add LSO grade to table. - table.insert(playerData.grades, mygrade) + -- Add LSO grade to player grades table. + table.insert(self.playerscores[playerData.name], mygrade) -- LSO grade: (OK) 3.0 PT - LURIM local text=string.format("%s %.1f PT - %s", grade, Points, analysis) @@ -8537,6 +8645,11 @@ function AIRBOSS:_Debrief(playerData) -- Debug message. MESSAGE:New(string.format("Player step %s.", playerData.step), 5, "DEBUG"):ToAllIf(self.Debug) + + -- Auto save player results. + if self.autosave and mygrade.finalscore then + self:Save(self.autosavepath, self.autosavefile) + end end --- Hind for flight students about the (next) step. @@ -9419,9 +9532,13 @@ function AIRBOSS:_AddF10Commands(_unitName) if self.menumarkzones then local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) -- F10/Airboss//F1 Help/F1 Mark Zones/ + if self.menusmokezones then missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 - missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 + end + missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 + if self.menusmokezones then missionCommands.addCommandForGroup(gid, "Smoke Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F3 + end missionCommands.addCommandForGroup(gid, "Flare Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F4 end -- F10/Airboss//F1 Help/F2 Skill Level @@ -9832,22 +9949,30 @@ function AIRBOSS:_DisplayScoreBoard(_unitName) local _playerResults={} -- Calculate average points for all players. - for _playerName,_playerData in pairs(self.players) do - local playerData=_playerData --#AIRBOSS.PlayerData + for playerName,playerGrades in pairs(self.playerscores) do + + if playerGrades then - local Paverage=0 - local n=0 - for _,_grade in pairs(playerData.grades) do - local grade=_grade --#AIRBOSS.LSOgrade + -- Loop over all grades + local Paverage=0 + local n=0 + for _,_grade in pairs(playerGrades) do + local grade=_grade --#AIRBOSS.LSOgrade + + -- Add up points if >=0. For foul deck WO we give -1 and it does not count. + if grade.finalscore then --grade.points>=0 then + Paverage=Paverage+grade.finalscore + n=n+1 + else + --TODO: handle case when the player just leaves after an unfinished pass, e.g bolter, without landing. + end + end + + -- We dont want to devide by zero. + if n>0 then + _playerResults[playerName]=Paverage/n + end - -- Add up points if >=0. For foul deck WO we give -1 and it does not count. - if grade.points>=0 then - Paverage=Paverage+grade.points - n=n+1 - end - end - if n>0 then - _playerResults[_playerName]=Paverage/n end end @@ -9856,28 +9981,31 @@ function AIRBOSS:_DisplayScoreBoard(_unitName) local i=1 for _playerName,_points in UTILS.spairs(_playerResults, function(t, a, b) return t[b] < t[a] end) do - -- Current player data. - local playerData=self.players[_playerName] --#AIRBOSS.PlayerData - - if playerData then - - -- Text. - text=text..string.format("\n[%d] %s %.1f|", i,_playerName,_points) + -- Text. + text=text..string.format("\n[%d] %s %.1f||", i,_playerName, _points) + + -- All player grades. + local playerGrades=self.playerscores[_playerName] - -- Add grades of passes. - for _,_grade in pairs(playerData.grades) do - local grade=_grade --#AIRBOSS.LSOgrade - text=text..string.format("|%.1f", grade.points) - end - - i=i+1 - if i>10 then - break + -- Add grades of passes. We use the actual grade of each pass here and not the average after player has landed. + for _,_grade in pairs(playerGrades) do + local grade=_grade --#AIRBOSS.LSOgrade + if grade.finalscore then + text=text..string.format("%.1f|", grade.points) + else + text=text..string.format("(%.1f)", grade.points) end + end + -- Display only the top ten. + i=i+1 + if i>10 then + break end end - if i==0 then + + -- If no results yet. + if i==1 then text=text.."\nNo results yet." end @@ -9906,12 +10034,18 @@ function AIRBOSS:_DisplayPlayerGrades(_unitName) if playerData then -- Grades of player: - local text=string.format("Your grades, %s:", _playername) + local text=string.format("Your last 10 grades, %s:", _playername) + + -- All player grades. + local playerGrades=self.playerscores[_playername] or {} local p=0 -- Average points. - local n=0 -- Number of valid passes. - for i,_grade in pairs(playerData.grades) do - local grade=_grade --#AIRBOSS.LSOgrade + local n=0 -- Number of final passes. + local m=0 -- Number of total passes. + --for i,_grade in pairs(playerGrades) do + for i=#playerGrades,1,-1 do + --local grade=_grade --#AIRBOSS.LSOgrade + local grade=playerGrades[i] --#AIRBOSS.LSOgrade -- Check if points >=0. For foul deck WO we give -1 and pass is not counted. if grade.points>=0 then @@ -9919,21 +10053,29 @@ function AIRBOSS:_DisplayPlayerGrades(_unitName) -- Show final points or points of pass. local points=grade.finalscore or grade.points - text=text..string.format("\n[%d] %s %.1f PT - %s", n+1, grade.grade, points, grade.details) - - -- Wire trapped if any. - if grade.wire and grade.wire<=4 then - text=text..string.format(" %d-wire", grade.wire) + -- Display max 10 results. + if m<10 then + text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, points, grade.details) + + -- Wire trapped if any. + if grade.wire and grade.wire<=4 then + text=text..string.format(" %d-wire", grade.wire) + end + + -- Time in the groove if any. + if grade.Tgroove and grade.Tgroove<=60 then + text=text..string.format(" Tgroove=%.1f s", grade.Tgroove) + end end - -- Time in the groove if any. - if grade.Tgroove and grade.Tgroove<=60 then - text=text..string.format(" Tgroove=%.1f s", grade.Tgroove) + -- Add up final points. + if grade.finalscore then + p=p+grade.finalscore + n=n+1 end - -- Add up points. - p=p+grade.points - n=n+1 + -- Total passes + m=m+1 end end @@ -10304,6 +10446,14 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) local stackalt=self:_GetMarshalAltitude(stack) local angels=self:_GetAngels(stackalt) stacktext=string.format("Marshal Stack %d, Angels %d\n", stack, angels) + + + -- Hint about TACAN bearing. + if playerData.holding~=nil and playerData.case>1 then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(playerData.case, true, true, true) + stacktext=stacktext..string.format("Select TACAN %03d°, DME %d NM\n", radial, angels+15) + end end -- Fuel and fuel state. @@ -10579,6 +10729,248 @@ function AIRBOSS:_MarshalRadioCheck(_unitName) end end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +-- Persistence Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + +--- On before "Save" event. Checks if io and lfs are available. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onbeforeSave(From, Event, To, path, filename) + + -- Check io module is available. + if not io then + self:E(self.lid.."ERROR: io not desanitized. Can't save player grades.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\DCS\" folder.") + end + + return true +end + +--- On after "Save" event. Player data is saved to file. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory or your "Saved Games" folder if lfs was desanitized. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onafterSave(From, Event, To, path, filename) + + --- Function that saves data to file + local function _savefile(filename, data) + local f = assert(io.open(filename, "wb")) + f:write(data) + f:close() + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set file name. + filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info + local text=string.format("Saving player LSO grades to file %s", filename) + MESSAGE:New(text,30):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Header line + local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case\n" + + -- Loop over all players. + for playername,grades in pairs(self.playerscores) do + + -- Loop over player grades table. + for i,_grade in pairs(grades) do + local grade=_grade --#AIRBOSS.LSOgrade + + -- Check some stuff that could be nil. + local wire="n/a" + if grade.wire and grade.wire<=4 then + wire=tostring(grade.wire) + end + + local Tgroove="n/a" + if grade.Tgroove and grade.Tgroove<=60 and grade.case<3 then + Tgroove=tostring(UTILS.Round(grade.Tgroove, 1)) + end + + local finalscore="n/a" + if grade.finalscore then + finalscore=tostring(UTILS.Round(grade.finalscore, 1)) + end + + -- Compile grade line. + scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d\n", playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case) + end + end + + -- Save file. + _savefile(filename, scores) +end + + +--- On before "Load" event. Checks if the file that the player grades from exists. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path (Optional) Path where the file is loaded from. Default is the DCS installation root directory or your "Saved Games\\DCS" folder if lfs was desanizized. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onbeforeLoad(From, Event, To, path, filename) + + --- Function that check if a file exists. + local function _fileexists(name) + local f=io.open(name,"r") + if f~=nil then + io.close(f) + return true + else + return false + end + end + + -- Check io module is available. + if not io then + self:E(self.lid.."ERROR: io not desanitized. Can't load player grades.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\DCS\" folder.") + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set file name. + filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=_fileexists(filename) + + if exists then + return true + else + self:E(self.lid..string.format("WARNING: Player LSO grades file %s does not exist.", filename), 60) + return false + end + +end + + +--- On after "Load" event. Loads grades of all players from file. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is loaded from. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if lfs was desanizied. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onafterLoad(From, Event, To, path, filename) + + --- Function that load data from a file. + local function _loadfile(filename) + local f=assert(io.open(filename, "rb")) + local data=f:read("*all") + f:close() + return data + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set file name. + filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info message. + local text=string.format("Loading player LSO grades from file %s", filename) + MESSAGE:New(text,10):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Load asset data from file. + local data=_loadfile(filename) + + -- Split by line break. + local playergrades=UTILS.Split(data,"\n") + + -- Remove first header line. + table.remove(playergrades, 1) + + -- Init player scores table. + self.playerscores={} + + -- Loop over all lines. + for _,gradeline in pairs(playergrades) do + + -- Parameters are separated by commata. + local gradedata=UTILS.Split(gradeline, ",") + + -- Debug info. + self:T2(gradedata) + + -- Grade table + local grade={} --#AIRBOSS.LSOgrade + + -- Line format: playername, i, grade.finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, case + local playername=gradedata[1] + if gradedata[3]~=nil and gradedata[3]~="n/a" then + grade.finalscore=tonumber(gradedata[3]) + end + grade.points=tonumber(gradedata[4]) + grade.grade=tostring(gradedata[5]) + grade.details=tostring(gradedata[6]) + if gradedata[7]~=nil and gradedata[7]~="n/a" then + grade.wire=tonumber(gradedata[7]) + end + if gradedata[8]~=nil and gradedata[8]~="n/a" then + grade.Tgroove=tonumber(gradedata[8]) + end + grade.case=tonumber(gradedata[9]) + + -- Init player table if necessary. + self.playerscores[playername]=self.playerscores[playername] or {} + + -- Add grade to table. + table.insert(self.playerscores[playername], grade) + + -- Debug info. + self:T2({playername, self.playerscores[playername]}) + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ From dd15da28b28bfd4c9b26b7b670b6bbdb75ead296 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 20 Jan 2019 02:02:22 +0100 Subject: [PATCH 135/147] AIBOSS v0.9.0 SPAWN: Added delay for respawn on landing. RAT v2.3.5: Fixed bug for Vmax cruise. RECOVERYTANKER v1.0.3 RESCUEHELO v1.0.2 POSITIONABLE: Added GetOffsetCoordinate function UNIT: Added Explode function. --- Moose Development/Moose/Core/Spawn.lua | 7 +- Moose Development/Moose/Functional/RAT.lua | 4 +- Moose Development/Moose/Ops/Airboss.lua | 2709 ++++++++++++----- .../Moose/Ops/RecoveryTanker.lua | 175 +- Moose Development/Moose/Ops/RescueHelo.lua | 160 +- .../Moose/Wrapper/Positionable.lua | 40 +- Moose Development/Moose/Wrapper/Unit.lua | 21 + 7 files changed, 2255 insertions(+), 861 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 04b1ed8db..d69ae0097 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -8,7 +8,7 @@ -- * Schedule spawning of new groups. -- * Put limits on the amount of groups that can be spawned, and the amount of units that can be alive at the same time. -- * Randomize the spawning location between different zones. --- * Randomize the intial positions within the zones. +-- * Randomize the initial positions within the zones. -- * Spawn in array formation. -- * Spawn uncontrolled (for planes or helos only). -- * Clean up inactive helicopters that "crashed". @@ -2672,7 +2672,10 @@ function SPAWN:_OnLand( EventData ) if self.RepeatOnLanding then local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) + --self:ReSpawn( SpawnGroupIndex ) + -- Delay respawn by three seconds due to DCS 2.5.4.26368 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 + -- Bug was initially only for engine shutdown event but after ED "fixed" it, it now happens on landing events. + SCHEDULER:New(nil, self.ReSpawn, {self, SpawnGroupIndex}, 3) end end end diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 9a1c9306b..60410bf24 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -546,7 +546,7 @@ RAT.id="RAT | " --- RAT version. -- @list version RAT.version={ - version = "2.3.4", + version = "2.3.5", print = true, } @@ -2446,7 +2446,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) local VxCruiseMax if self.Vcruisemax then -- User input. - VxCruiseMax = min(self.Vcruisemax, self.aircraft.Vmax) + VxCruiseMax = math.min(self.Vcruisemax, self.aircraft.Vmax) else -- Max cruise speed 90% of Vmax or 900 km/h whichever is lower. VxCruiseMax = math.min(self.aircraft.Vmax*0.90, 250) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 51e0612b9..ea4000e4b 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -7,7 +7,7 @@ -- * CASE I, II and III recoveries. -- * Supports human pilots as well as AI flight groups. -- * Automatic LSO grading (WIP) including (optional) live grading while in the groove. --- * Different skill levels from on-the-fly tips for flight students to *ziplip* for pros. +-- * Different skill levels from on-the-fly tips for flight students to *ziplip* for pros. Can be set for each player individually. -- * Define recovery time windows with individual recovery cases in the same mission. -- * Automatic TACAN and ICLS channel setting of carrier. -- * Separate radio channels for LSO and Marshal transmissions. @@ -35,22 +35,23 @@ -- * S-3B Viking & tanker version (AI) -- -- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) as aircraft and the USS John C. Stennis as carrier. --- The A-4E community mod is also supported in priciple but may need further tweaking of parameters. Also the A-4E-C mod needs *easy comms* activated to interact with the F10 radio menu. +-- The A-4E community mod is also supported in principle but may need further tweaking of parameters. Also the A-4E-C mod needs *easy comms* activated to interact with the F10 radio menu. -- --- The implemenation is kept general. So other aircraft and carriers possible in future. [*Winter is coming!*](https://forums.eagle.ru/forumdisplay.php?f=395) +-- The implementation is kept general. So other aircraft and carriers possible in future. [*Winter is coming!*](https://forums.eagle.ru/forumdisplay.php?f=395) -- But each aircraft or carrier needs a different set of optimized individual parameters. -- -- **PLEASE NOTE** that his class is work in progress and in an early **alpha** stage. Many/most things work already very nicely but there a lot of cases I did not run into yet. -- Therefore, your *constructive* feedback is both necessary and appreciated! -- --- ### Some Open Questions? +-- ### Open Questions? -- +-- * Currently the script does not support spin patterns. Marshal releases flights only when there is a free slot in the landing pattern. How is this handled in real life? -- * What is the next step after a pattern wave off during Case II or III recovery? --- * What is the condition for a "fly through" (\\ or /) LSO grade? --- * The above question is one of many regarding LSO grade. If you have more info, please share. +-- * What are the conditions for waving off flights when they get too close to a flight ahead in the pattern? At which pattern steps are flights waved off because of this? +-- * Some more LSO gradings could be added. What is missing and what are the conditions? +-- * For the A-4E-C, what are the AoA thresholds for being on speed, (a little) slow, (a little) fast in **degrees**? In know the numbers in units of the indexer but need a proper conversion to degrees. -- --- If you know the answer to any of this, please get in touch with me! --- The necessary infrastructure to implement it is most likely already there, but I am not 100% sure about the exact conditions. +-- If you know the answer to any of this, please get in touch with me! The necessary infrastructure to implement it is most likely already there. -- -- === -- @@ -70,7 +71,7 @@ -- @field #string lid Class id string for output to DCS log file. -- @field Wrapper.Unit#UNIT carrier Aircraft carrier unit on which we want to practice. -- @field #string carriertype Type name of aircraft carrier. --- @field #AIRBOSS.CarrierParameters carrierparam Carrier specifc parameters. +-- @field #AIRBOSS.CarrierParameters carrierparam Carrier specific parameters. -- @field #string alias Alias of the carrier. -- @field Wrapper.Airbase#AIRBASE airbase Carrier airbase object. -- @field #table waypoints Waypoint coordinates of carrier. @@ -96,7 +97,7 @@ -- @field #table menuadded Table of units where the F10 radio menu was added. -- @field #AIRBOSS.Checkpoint BreakEntry Break entry checkpoint. -- @field #AIRBOSS.Checkpoint BreakEarly Early break checkpoint. --- @field #AIRBOSS.Checkpoint BreakLate Late brak checkpoint. +-- @field #AIRBOSS.Checkpoint BreakLate Late break checkpoint. -- @field #AIRBOSS.Checkpoint Abeam Abeam checkpoint. -- @field #AIRBOSS.Checkpoint Ninety At the ninety checkpoint. -- @field #AIRBOSS.Checkpoint Wake Checkpoint right behind the carrier. @@ -113,9 +114,13 @@ -- @field #table flights List of all flights in the CCA. -- @field #table Qmarshal Queue of marshalling aircraft groups. -- @field #table Qpattern Queue of aircraft groups in the landing pattern. +-- @field #table Qwaiting Queue of aircraft groups waiting outside 10 NM zone for the next free Marshal stack. -- @field #table RQMarshal Radio queue of marshal. -- @field #table RQLSO Radio queue of LSO. -- @field #number Nmaxpattern Max number of aircraft in landing pattern. +-- @field #number Nmaxmarshal Number of max Case I Marshal stacks available. Default 3, i.e. angels 2, 3 and 4. +-- @field #number NmaxSection Number of max section members (excluding the lead itself), i.e. NmaxSection=1 is a section of two. +-- @field #number NmaxStack Number of max flights per stack. Default 2. -- @field #boolean handleai If true (default), handle AI aircraft. -- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. -- @field Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. @@ -137,6 +142,11 @@ -- @field #boolean autosave If true, all player grades are automatically saved to a file on disk. -- @field #string autosavepath Path where the player grades file is saved on auto save. -- @field #string autosavefilename File name of the auto player grades save file. Default is auto generated from carrier name/alias. +-- @field #number marshalradius Radius of the Marshal stack zone. +-- @field #boolean airbossnice Airboss is a nice guy. +-- @field #boolean staticweather Mission uses static rather than dynamic weather. +-- @field #number windowcount Running number counting the recovery windows. +-- @field #number LSOdT Time interval in seconds before the LSO will make its next call. -- @extends Core.Fsm#FSM --- Be the boss! @@ -172,7 +182,7 @@ -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Holding.png) -- -- The graphic depicts a the standard holding pattern during a Case I recovery. Incoming aircraft enter the holding pattern, which is a counter clockwise turn with a --- diameter of 5 NM, at their assigned altiude. The holding altitude of the first stack is 2000 ft. The inverval between stacks is 1000 ft. +-- diameter of 5 NM, at their assigned altitude. The holding altitude of the first stack is 2000 ft. The interval between stacks is 1000 ft. -- -- Once a recovery window opens, the aircraft of the lowest stack commence their landing approach and the rest of the Marshal stack collapses, i.e. aircraft switch from -- their current stack to the next lower stack. @@ -186,97 +196,34 @@ -- -- Once the aircraft reaches the Inital, the landing pattern begins. The important steps of the pattern are shown in the image above. -- +-- -- ## CASE III -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3.png) -- --- A Case III recovery is conducted during nighttime. The holding positon and the landing pattern are rather different from a Case I recovery as can be seen in the image above. +-- A Case III recovery is conducted during nighttime. The holding position and the landing pattern are rather different from a Case I recovery as can be seen in the image above. -- --- The first holding zone starts 21 NM astern the carrier at angels 6. The interval between the stacks is 1000 ft just like in Case I. However, the distance to the boat +-- The first holding zone starts 21 NM astern the carrier at angels 6. The separation between the stacks is 1000 ft just like in Case I. However, the distance to the boat -- increases by 1 NM with each stack. The general form can be written as D=15+6+(N-1), where D is the distance to the boat in NM and N the number of the stack starting at N=1. -- -- Once the aircraft of the lowest stack is allowed to commence to the landing pattern, it starts a descent at 4000 ft/min until it reaches the "*Platform*" at 5000 ft and --- ~19 NM DME. From there a shallower descent at 2000 ft/min should be performed. At an altitude of 1200 ft the aircraft should level out and "*Dirty Up*" (gear & hook down). +-- ~19 NM DME. From there a shallower descent at 2000 ft/min should be performed. At an altitude of 1200 ft the aircraft should level out and "*Dirty Up*" (gear, flaps & hook down). -- --- At 3 NM distance to the carrier, the aircraft should intercept the 3.5 degrees glide slope at the "*Bullseye*". From there the pilot should "follow the needes" of the ICLS. +-- At 3 NM distance to the carrier, the aircraft should intercept the 3.5 degrees glideslope at the "*Bullseye*". From there the pilot should "follow the needles" of the ICLS. -- -- ## CASE II -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case2.png) -- --- Case II is the common recovery procedure at daytime if visibilty conditions are poor. It can be viewed as hybrid between Case I and III. +-- Case II is the common recovery procedure at daytime if visibility conditions are poor. It can be viewed as hybrid between Case I and III. -- The holding pattern is very similar to that of the Case III recovery with the difference the the radial is the inverse of the BRC instead of the FB. -- From the holding zone aircraft are follow the Case III path until they reach the Initial position 3 NM astern the boat. From there a standard Case I recovery procedure is -- in place. -- --- Note that the image depicts the case, where the holding zone has an angle offset of 30 degrees with respect to the BRC. This is optional. Commonly used offset angles +-- Note that the image depicts the case, where the holding zone has an angle offset of 30 degrees with respect to the BRC. This is optional. Commonly used offset angels -- are 0 (no offset), +-15 or +-30 degrees. The AIRBOSS class supports all these scenarios which are used during Case II and III recoveries. -- --- --- # Scripting --- --- Writing a basic script is easy and can be done in two lines. --- --- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") --- airbossStennis:Start() --- --- The **first line** creates and AIRBOSS object via the @{#AIRBOSS.New}(*carriername*, *alias*) constructor. The first parameter *carriername* is name of the carrier unit as --- defined in the mission editor. The second parameter *alias* is optional. This name will, e.g., be used for the F10 radio menu entry. If not given, the alias is identical --- to the *carriername* of the first parameter. --- --- This simple script initializes a lot of parameters with default values: --- --- * TACAN channel is set to 74X, see @{#AIRBOSS.SetTACAN}, --- * ICSL channel is set to 1, see @{#AIRBOSS.SetICLS}, --- * LSO radio is set to 264 MHz FM, see @{#AIRBOSS.SetLSORadio}, --- * Marshal radio is set to 305 MHz FM, see @{#AIRBOSS.SetMarshalRadio}, --- * Default recovery case is set to 1, see @{#AIRBOSS.SetRecoveryCase}, --- * Carrier Controlled Area (CCA) is set to 50 NM, see @{#AIRBOSS.SetCarrierControlledArea}, --- * Default player skill "Flight Student" (easy), see @{#AIRBOSS.SetDefaultPlayerSkill}, --- * Once the carrier reaches its final waypoint, it will restart its route, see @{#AIRBOSS.SetPatrolAdInfinitum}. --- --- The **second line** starts the AIRBOSS class. If you set options this should happen after the @{#AIRBOSS.New} and before @{#AIRBOSS.Start} command. --- --- If no recovery window is set like in the basic example, a window will automatically open 15 minutes after mission start and close again after three hours. --- The next section explains how to set your own recovery times. --- --- ## Recovery Windows --- --- Recovery of aircraft is only allowed during defined time slots. You can define these slots via the @{#AIRBOSS.AddRecoveryWindow}(*start*, *stop*, *case*, *holdingoffset*) function. --- The parameters are: --- --- * *start*: The start time as a string. For example "8:00" for a window opening at 8 am. Or "13:30+1" for half past one on the next day. Default (nil) is ASAP. --- * *stop*: Time when the window closes as a string. Same format as *start*. Default is 90 minutes after start time. --- * *case*: The recovery case during that window (1, 2 or 3). Default 1. --- * *holdingoffset*: Holding offset angle in degrees. Only for Case II or III recoveries. Default 0 deg. Common +-15 deg or +-30 deg. --- --- If recovery is closed, AI flights will be send to marshal stacks and orbit there until the next window opens. --- Players can request marshal via the F10 menu and will also be given a marshal stack. Currently, human players can request commence via the F10 radio regarless of --- whether a window is open or not and will be alowed to enter the pattern (if not already full). This will probably change in the future. --- --- At the moment there is no autmatic recovery case set depending on weather or daytime. So it is the AIRBOSS (you) who needs to make that descision. --- It is probably a good idea to synchronize the timing with the waypoints of the carrier. For example, setting up the waypoints such that the carrier --- already has turning into the wind, when a recovery window opens. --- --- The code for setting up multiple recovery windows could look like this --- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") --- airbossStennis:AddRecoveryWindow("8:30", "9:30", 1) --- airbossStennis:AddRecoveryWindow("12:00", "13:15", 2, 15) --- airbossStennis:AddRecoveryWindow("23:30", "00:30+1", 3, -30) --- airbossStennis:Start() --- --- This will open a Case I recovery window from 8:30 to 9:30. Then a Case II recovery from 12:00 to 13:15, where the holing offset is +15 degrees wrt BRC. --- Finally, a Case III window opens 23:30 on the day the mission starts and closes 0:30 on the following day. The holding offset is -30 degrees wrt FB. --- --- Note that incoming flights will be assigned a holding pattern for the next opening window case if no window is open at the moment. So in the above example, --- all flights incoming after 13:15 will be assigned to a Case III marshal stack. Therefore, you should make sure that no flights are incoming long before the --- next window opens or adjust the recovery planning accordingly. --- --- The following example shows how you set up a recovery window for the next week: --- --- for i=0,7 do --- AddRecoveryWindow(string.format("08:05:00+%d", i), string.format("08:50:00+%d", i)) --- end +-- === -- -- # The F10 Radio Menu -- @@ -300,6 +247,7 @@ -- -- This radio command can be used to request a stack in the holding pattern from Marshal. Necessary conditions are that the flight is inside the Carrier Controlled Area (CCA) -- (see @{#AIRBOSS.SetCarrierControlledArea}). +-- -- Marshal will assign an individual stack for each player group depending on the current or next open recovery case window. -- If multiple players have registered as a section, the section lead will be assigned a stack and is responsible to guide his section to the assigned holding position. -- @@ -309,11 +257,11 @@ -- and that the number of aircraft in the landing pattern is smaller than four. -- -- A player can also request commencing if he is not registered in a marshal stack yet. If the pattern is free, Marshal will allow him to directly enter the landing pattern. --- +-- -- ### Request Refueling -- -- If a recovery taker has been set up via the @{#AIRBOSS.SetRecoveryTanker}, the player can request refueling at any time. If currently in the marshal stack, the stack above will collapse. --- The player will be informed if the tanker is currently busy or going RTB to refuel itsel at its home base. Once the re-fueling is complete, the player has to re-register to the marshal stack. +-- The player will be informed if the tanker is currently busy or going RTB to refuel itself at its home base. Once the re-fueling is complete, the player has to re-register to the marshal stack. -- -- ### [Reset My Status] -- @@ -362,10 +310,10 @@ -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuAttitudeMonitor.png) -- --- This command displays the current aircraft attitude of the player in short intervals as message on the screen. --- It provides information about current pitch, roll, yaw, lineup and glideslope error, orientation of the plane wrt to carrier etc. +-- This command displays the current aircraft attitude of the player aircraft in short intervals as message on the screen. +-- It provides information about current pitch, roll, yaw, orientation of the plane with respect to the carrier's orientation (*Gamma*) etc. -- --- If you are in the groove, current lineup and glide slope errors are displayed and you get an on-the-fly LSO grade. +-- If you are in the groove, current lineup and glideslope errors are displayed and you get an on-the-fly LSO grade. -- -- ### LSO Radio Check -- @@ -395,7 +343,7 @@ -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuCarrierInfo.png) -- --- Information about the current carrier status is displayed. This includes current BRC, FB, LSO and Marshal frequences, list of next recovery windows. +-- Information about the current carrier status is displayed. This includes current BRC, FB, LSO and Marshal frequencies, list of next recovery windows. -- -- ### Weather Report -- @@ -403,33 +351,62 @@ -- -- Displays information about the current weather at the carrier such as QFE, wind and temperature. -- +-- For missions using static weather, more information such as cloud base, thickness, precipitation, visibility distance, fog and dust are displayed. +-- If you mission uses dynamic weather, you can disable this output via the @{#AIRBOSS.SetStaticWeather}(**false**) function. +-- -- ### Set Section -- -- With this command, you can define a section of human flights. The player how issues the command becomes the section lead and all other human players --- within a radius of 200 meters become members of the section. +-- within a radius of 100 meters become members of the section. +-- +-- The responsibilities of the section leader are: +-- +-- * To request Marshal. The section members are not allowed to do this and have to follow the lead to his assigned stack. +-- * To lead the right way to the pattern if the flight is allowed to commence. +-- * The lead is also the only one who can request commence if the flight wants to bypass the Marshal stack. +-- +-- Each time the command is issued by the lead, the complete section is set up from scratch. Members which are not inside the 100 m radius any more are +-- removed and/or new members which are now in range are added. +-- +-- If a section member issues this command, it is removed from the section of his lead. All flights which are not yet in another section will become members. +-- +-- The default maximum size of a section is two human players. This can be adjusted by the @{#AIRBOSS.SetMaxSectionSize}(*size*) function. The maximum allowed size +-- is four. -- -- ### Marshal Queue -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMarshalQueue.png) -- --- Lists all flights currently in the Marshal queue including their assigned stack, recovery case and Charie time estimate. +-- Lists all flights currently in the Marshal queue including their assigned stack, recovery case and Charie time estimate. +-- By default, the number of available Case I stacks is three, i.e. at angels 2, 3 and 4. Usually, the recovery thanker orbits at angels 6. +-- The number of available stacks can be set by the @{#AIRBOSS.SetMaxMarshalStack} function. +-- +-- The default number of human players per stack is two. This can be set via the @{#AIRBOSS.SetMaxFlightsPerStack} function but has to be between one and four. +-- +-- Due to technical reasons, each AI group always gets its own stack. DCS does not allow to control the AI in a manner that more than one group per stack would make sense unfortunately. -- -- ### Pattern Queue -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuPatternQueue.png) -- -- Lists all flights currently in the landing pattern queue showing the time since they entered the pattern. +-- By default, a maximum of four flights is allowed to enter the pattern. This can be set via the @{#AIRBOSS.SetMaxLandingPattern} function. +-- +-- ### Waiting Queue +-- +-- Lists all flights currently waiting for a free Case I Marshal stack. Note, stacks are limited only for Case I recovery ops but not for Case II or III. +-- If the carrier is switches recovery ops form Case I to Case II or III, all waiting flights will be assigned a stack. -- -- # Landing Signal Officer (LSO) -- -- The LSO will first contact you on his radio channel when you are at the the abeam position (Case I) with the phrase "Paddles, contact.". --- Once you are in the groove the LSO will ask you to "Call the ball." and then acknoledge your ball call by "Roger Ball." +-- Once you are in the groove the LSO will ask you to "Call the ball." and then acknowledge your ball call by "Roger Ball." -- -- During the groove the LSO will give you advice if you deviate from the correct landing path. These advices will be given when you are -- -- * too low or too high with respect to the glideslope, -- * too fast or too slow with respect to the optimal AoA, --- * too far left or too far right wirth respect to the lineup of the (angled) runway. +-- * too far left or too far right with respect to the lineup of the (angled) runway. -- -- ## LSO Grading -- @@ -446,7 +423,7 @@ -- * **L**ined **U**p **L**eft or **R**ight: LUL, LUR -- * Too **H**igh or too **LO**w: H, LO -- * Too **F**ast or too **SLO**w: F, SLO --- * **Fly through** glide slope **down** or **up**: \\ , / +-- * **Fly through** glideslope **down** or **up**: \\ , / -- -- Each grading, x, is subdivided by -- @@ -455,16 +432,16 @@ -- -- The position at the landing event is analyzed and the corresponding trapped wire calculated. If no wire was caught, the LSO will give the bolter call. -- --- If a player is sigifiantly off from the ideal parameters from IC to AR, the LSO will wave the player off. Thresholds for wave off are +-- If a player is significantly off from the ideal parameters from IC to AR, the LSO will wave the player off. Thresholds for wave off are -- -- * Line up error > 3.0 degrees left or right and/or --- * Glide slope error < -1.2 degrees or > 1.8 degrees and/or +-- * Glideslope error < -1.2 degrees or > 1.8 degrees and/or -- * AOA depending on aircraft type and only applied if skill level is "TOPGUN graduate". -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_LSOPlatcam.png) -- --- Line up and glide slope error thresholds were tested extensively using [VFA-113 Stingers LSO Mod](https://forums.eagle.ru/showthread.php?t=211557), --- if the aircraft is outside the red box. In the picture above, **blue** numbers denote the line up thresholds while the **blacks** refer to the glide slope. +-- Line up and glideslope error thresholds were tested extensively using [VFA-113 Stingers LSO Mod](https://forums.eagle.ru/showthread.php?t=211557), +-- if the aircraft is outside the red box. In the picture above, **blue** numbers denote the line up thresholds while the **blacks** refer to the glideslope. -- -- A wave off is called, when the aircraft is outside the red rectangle. The measurement stops already ~50 m before the rundown, since the error in the calculation -- increases the closer the aircraft gets to the origin/reference point. @@ -492,7 +469,7 @@ -- Currently grades are given by as follows -- -- * 5.0 Points **\_OK\_**: "Okay underline", given only for a perfect pass, i.e. when no deviations at all were observed by the LSO. The unicorn! --- * 4.0 Points **OK**: "Okay pass" when only minor () deviations happend. +-- * 4.0 Points **OK**: "Okay pass" when only minor () deviations happened. -- * 3.0 Points **(OK)**: "Fair pass", when only "normal" deviations were detected. -- * 2.0 Points **--**: "No grade", for larger deviations. -- @@ -510,32 +487,74 @@ -- -- The approaching aircraft will be notified via LSO radio comms and is supposed to overfly the landing area to enter the Bolter pattern. **This pass is not graded**. -- --- # AI Handling +-- === -- --- The @{#AIRBOSS} class allows to handle incoming AI units and integrate them into the marshal and landing pattern. +-- # Scripting -- --- By default, incoming carrier capable aircraft which are detecting inside the Carrier Controlled Area (CCA) and approach the carrier by more than 5 NM are automatically guided to the holding zone. --- Each AI group gets its own marshal stack in the holding pattern. Once a recovery window opens, the AI group of the lowest stack is transitioning to the landing pattern --- and the Marshal stack collapses. +-- Writing a basic script is easy and can be done in two lines. -- --- If no AI handling is desired, this can be turned off via the @{#AIRBOSS.SetHandleAIOFF} function. +-- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") +-- airbossStennis:Start() +-- +-- The **first line** creates and AIRBOSS object via the @{#AIRBOSS.New}(*carriername*, *alias*) constructor. The first parameter *carriername* is name of the carrier unit as +-- defined in the mission editor. The second parameter *alias* is optional. This name will, e.g., be used for the F10 radio menu entry. If not given, the alias is identical +-- to the *carriername* of the first parameter. -- --- ## Known Issues +-- This simple script initializes a lot of parameters with default values: -- --- Dealing with the DCS AI is a big challenge and there is only so much one can do. Please bear this in mind! +-- * TACAN channel is set to 74X, see @{#AIRBOSS.SetTACAN}, +-- * ICSL channel is set to 1, see @{#AIRBOSS.SetICLS}, +-- * LSO radio is set to 264 MHz FM, see @{#AIRBOSS.SetLSORadio}, +-- * Marshal radio is set to 305 MHz FM, see @{#AIRBOSS.SetMarshalRadio}, +-- * Default recovery case is set to 1, see @{#AIRBOSS.SetRecoveryCase}, +-- * Carrier Controlled Area (CCA) is set to 50 NM, see @{#AIRBOSS.SetCarrierControlledArea}, +-- * Default player skill "Flight Student" (easy), see @{#AIRBOSS.SetDefaultPlayerSkill}, +-- * Once the carrier reaches its final waypoint, it will restart its route, see @{#AIRBOSS.SetPatrolAdInfinitum}. +-- +-- The **second line** starts the AIRBOSS class. If you set options this should happen after the @{#AIRBOSS.New} and before @{#AIRBOSS.Start} command. -- --- ### Pattern Updates +-- If no recovery window is set like in the basic example, a window will automatically open 15 minutes after mission start and close again after three hours. +-- The next section explains how to set your own recovery times. +-- +-- ## Recovery Windows -- --- The holding position of the AI is updated regularly when the carrier has changed its position by more then 2.5 NM or changed its course significantly. --- The patterns are realized by orbit or racetrack patterns of the DCS scripting API. --- However, when the position is updated or the marshal stack collapses, it comes to disruptions of the regular orbit because a new waypoint with a new --- orbit task needs to be created. +-- Recovery of aircraft is only allowed during defined time slots. You can define these slots via the @{#AIRBOSS.AddRecoveryWindow}(*start*, *stop*, *case*, *holdingoffset*) function. +-- The parameters are: -- --- ### Recovery Cases +-- * *start*: The start time as a string. For example "8:00" for a window opening at 8 am. Or "13:30+1" for half past one on the next day. Default (nil) is ASAP. +-- * *stop*: Time when the window closes as a string. Same format as *start*. Default is 90 minutes after start time. +-- * *case*: The recovery case during that window (1, 2 or 3). Default 1. +-- * *holdingoffset*: Holding offset angle in degrees. Only for Case II or III recoveries. Default 0 deg. Common +-15 deg or +-30 deg. +-- +-- If recovery is closed, AI flights will be send to marshal stacks and orbit there until the next window opens. +-- Players can request marshal via the F10 menu and will also be given a marshal stack. Currently, human players can request commence via the F10 radio regardless of +-- whether a window is open or not and will be allowed to enter the pattern (if not already full). This will probably change in the future. -- --- The AI performs a very realistic Case I recovery. Therefore, we already have a good Case I and II recovery simulation since the final part of Case II is a --- Case I recovery. However, I don't think the AI can do a proper Case III recovery. If you give the AI the landing command, it is out of our hands and will --- always go for a Case I in the final pattern part. Maybe this will improve in future DCS version but right now, there is not much we can do about it. +-- At the moment there is no automatic recovery case set depending on weather or daytime. So it is the AIRBOSS (you) who needs to make that decision. +-- It is probably a good idea to synchronize the timing with the waypoints of the carrier. For example, setting up the waypoints such that the carrier +-- already has turning into the wind, when a recovery window opens. +-- +-- The code for setting up multiple recovery windows could look like this +-- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") +-- airbossStennis:AddRecoveryWindow("8:30", "9:30", 1) +-- airbossStennis:AddRecoveryWindow("12:00", "13:15", 2, 15) +-- airbossStennis:AddRecoveryWindow("23:30", "00:30+1", 3, -30) +-- airbossStennis:Start() +-- +-- This will open a Case I recovery window from 8:30 to 9:30. Then a Case II recovery from 12:00 to 13:15, where the holing offset is +15 degrees wrt BRC. +-- Finally, a Case III window opens 23:30 on the day the mission starts and closes 0:30 on the following day. The holding offset is -30 degrees wrt FB. +-- +-- Note that incoming flights will be assigned a holding pattern for the next opening window case if no window is open at the moment. So in the above example, +-- all flights incoming after 13:15 will be assigned to a Case III marshal stack. Therefore, you should make sure that no flights are incoming long before the +-- next window opens or adjust the recovery planning accordingly. +-- +-- The following example shows how you set up a recovery window for the next week: +-- +-- for i=0,7 do +-- AddRecoveryWindow(string.format("08:05:00+%d", i), string.format("08:50:00+%d", i)) +-- end +-- +-- === -- -- # Persistence of Player Results -- @@ -555,7 +574,7 @@ -- -- in the file "MissionScripting.lua", which is located in the subdirectory "Scripts" of your DCS installation root directory. -- --- ** WARNING ** Desanitizing the "io" and "lfs" modules makes your machine or server vunarable to attacks from the outside! Use this at your own risk. +-- **WARNING** Desanitizing the "io" and "lfs" modules makes your machine or server vulnerable to attacks from the outside! Use this at your own risk. -- -- ## Save Results -- @@ -597,6 +616,7 @@ -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_PersistenceResultsTable.png) -- -- The results file is stored as comma separated file. The columns are +-- -- * *Name*: The player name. -- * *Pass*: A running number counting the passes of the player -- * *Points Final*: The final points (i.e. when the player has landed). This is the average over all previous bolters or waveoffs, if any. @@ -610,21 +630,54 @@ -- ## Load Results -- -- Loading player grades from file is achieved by the @{AIRBOSS.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the --- data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory. +-- data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory or, if **lfs** was desanitized from you "Saved Games\DCS" directory. -- The parameter *filename* is optional and defines the name of the file to load. By default this is automatically generated from the AIBOSS carrier name/alias, for example -- "Airboss-USS Stennis_LSOgrades.csv". -- -- Note that the AIRBOSS FSM **must not be started** in order to load the data. In other words, loading should happen **after** the -- @{#AIRBOSS.New} command is specified in the code but **before** the @{#AIRBOSS.Start} command is given. -- --- Loading the player results is done by +-- The easiest was to load player results is -- -- airbossStennis:New("USS Stennis") --- airbossStennis:Load("D:\\My Airboss Data\\") +-- airbossStennis:Load() +-- airbossStennis:SetAutoSave() -- -- Additional specification of parameters such as recovery windows etc, if required. -- airbossStennis:Start() -- --- This sequence loads all available player grades from file. +-- This sequence loads all available player grades from the default file and automatically saved them when a player received a (final) grade. Again, if **lfs** was desanitized, the files are save to and loaded +-- from the "Saved Games\DCS" directory. If **lfs** was *not* desanitized, the DCS root installation folder is the default path. +-- +-- === +-- +-- # AI Handling +-- +-- The @{#AIRBOSS} class allows to handle incoming AI units and integrate them into the marshal and landing pattern. +-- +-- By default, incoming carrier capable aircraft which are detecting inside the Carrier Controlled Area (CCA) and approach the carrier by more than 5 NM are automatically guided to the holding zone. +-- Each AI group gets its own marshal stack in the holding pattern. Once a recovery window opens, the AI group of the lowest stack is transitioning to the landing pattern +-- and the Marshal stack collapses. +-- +-- If no AI handling is desired, this can be turned off via the @{#AIRBOSS.SetHandleAIOFF} function. +-- +-- ## Known Issues +-- +-- Dealing with the DCS AI is a big challenge and there is only so much one can do. Please bear this in mind! +-- +-- ### Pattern Updates +-- +-- The holding position of the AI is updated regularly when the carrier has changed its position by more then 2.5 NM or changed its course significantly. +-- The patterns are realized by orbit or racetrack patterns of the DCS scripting API. +-- However, when the position is updated or the marshal stack collapses, it comes to disruptions of the regular orbit because a new waypoint with a new +-- orbit task needs to be created. +-- +-- ### Recovery Cases +-- +-- The AI performs a very realistic Case I recovery. Therefore, we already have a good Case I and II recovery simulation since the final part of Case II is a +-- Case I recovery. However, I don't think the AI can do a proper Case III recovery. If you give the AI the landing command, it is out of our hands and will +-- always go for a Case I in the final pattern part. Maybe this will improve in future DCS version but right now, there is not much we can do about it. +-- +-- === -- -- # Debugging -- @@ -641,7 +694,7 @@ -- -- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. -- --- ## Debug Mode +-- ### Debug Mode -- -- You have the option to enable the debug mode for this class via the @{#AIRBOSS.SetDebugModeON} function. -- If enabled, status and debug text messages will be displayed on the screen. Also informative marks on the F10 map are created. @@ -696,9 +749,13 @@ AIRBOSS = { flights = {}, Qpattern = {}, Qmarshal = {}, + Qwaiting = {}, RQMarshal = {}, RQLSO = {}, Nmaxpattern = nil, + Nmaxmarshal = nil, + NmaxSection = nil, + NmaxStack = nil, handleai = nil, tanker = nil, warehouse = nil, @@ -720,6 +777,11 @@ AIRBOSS = { autosave = nil, autosavefile = nil, autosavepath = nil, + marshalradius = nil, + airbossnice = nil, + staticweather = nil, + windowcount = 0, + LSOdT = nil, } --- Player aircraft types capable of landing on carriers. @@ -766,7 +828,7 @@ AIRBOSS.AircraftCarrier={ -- @field #string KUZNETSOV Admiral Kuznetsov (CV 1143.5) AIRBOSS.CarrierType={ STENNIS="Stennis", - VINSON="Vinson", + VINSON="VINSON", TARAWA="LHA_Tarawa", KUZNETSOV="KUZNECOW", } @@ -803,6 +865,7 @@ AIRBOSS.CarrierType={ -- @field #string SPINNING "Spinning". -- @field #string COMMENCING "Commencing". -- @field #string HOLDING "Holding". +-- @field #string WAITING "Waiting for free Marshal stack". -- @field #string PLATFORM "Platform". -- @field #string ARCIN "Arc Turn In". -- @field #string ARCOUT "Arc Turn Out". @@ -829,6 +892,7 @@ AIRBOSS.PatternStep={ SPINNING="Spinning", COMMENCING="Commencing", HOLDING="Holding", + WAITING="Waiting for free Marshal stack", PLATFORM="Platform", ARCIN="Arc Turn In", ARCOUT="Arc Turn Out", @@ -871,7 +935,7 @@ AIRBOSS.GroovePos={ --- Radio sound file and subtitle. -- @type AIRBOSS.RadioCall -- @field #string file Sound file name without suffix. --- @field #string suffix File suffix/extention, e.g. "ogg". +-- @field #string suffix File suffix/extension, e.g. "ogg". -- @field #boolean loud Loud version of sound file available. -- @field #string subtitle Subtitle displayed during transmission. -- @field #number duration Duration of the sound in seconds. This is also the duration the subtitle is displayed. @@ -1065,7 +1129,7 @@ AIRBOSS.LSOCall={ suffix="ogg", loud=false, subtitle="", - duration=0.38, + duration=0.39, }, N6={ file="LSO-N6", @@ -1175,7 +1239,7 @@ AIRBOSS.MarshalCall={ suffix="ogg", loud=false, subtitle="", - duration=0.38, + duration=0.39, }, N6={ file="LSO-N6", @@ -1209,9 +1273,9 @@ AIRBOSS.MarshalCall={ --- Difficulty level. -- @type AIRBOSS.Difficulty --- @field #string EASY Flight Stutdent. Shows tips and hints in important phases of the approach. +-- @field #string EASY Flight Student. Shows tips and hints in important phases of the approach. -- @field #string NORMAL Naval aviator. Moderate number of hints but not really zip lip. --- @field #string HARD TOPGUN graduate. For people who know what they are doing. Nearly ziplip. +-- @field #string HARD TOPGUN graduate. For people who know what they are doing. Nearly *ziplip*. AIRBOSS.Difficulty={ EASY="Flight Student", NORMAL="Naval Aviator", @@ -1226,18 +1290,19 @@ AIRBOSS.Difficulty={ -- @field #number OFFSET Angle offset of the holding pattern in degrees. Usually 0, +-15, or +-30 degrees. -- @field #boolean OPEN Recovery window is currently open. -- @field #boolean OVER Recovery window is over and closed. +-- @field #number ID Recovery window ID. --- Groove data. -- @type AIRBOSS.GrooveData -- @field #number Step Current step. -- @field #number AoA Angle of Attack. -- @field #number Alt Altitude in meters. --- @field #number GSE Glide slope error in degrees. +-- @field #number GSE Glideslope error in degrees. -- @field #number LUE Lineup error in degrees. -- @field #number Roll Roll angle. -- @field #number Rhdg Relative heading player to carrier. 0=parallel, +-90=perpendicular. -- @field #number TGroove Time stamp when pilot entered the groove. --- @field #string FlyThrough Fly through up "/" or fly through down "\". +-- @field #string FlyThrough Fly through up "/" or fly through down "\\". --- LSO grade -- @type AIRBOSS.LSOgrade @@ -1267,7 +1332,7 @@ AIRBOSS.Difficulty={ -- @field #string groupname Name of the group. -- @field #number nunits Number of units in group. -- @field #number dist0 Distance to carrier in meters when the group was first detected inside the CCA. --- @field #number time Timestamp in seconds of timer.AbsTime() of the last important event, e.g. added to the queue. +-- @field #number time Timestamp in seconds of timer.getAbsTime() of the last important event, e.g. added to the queue. -- @field Core.UserFlag#USERFLAG flag User flag for triggering events for the flight. -- @field #boolean ai If true, flight is purly AI. -- @field #string actype Aircraft type name. @@ -1311,33 +1376,38 @@ AIRBOSS.Difficulty={ -- @field #number Tlso Last time the LSO gave an advice. -- @field #number Tgroove Time in the groove in seconds. -- @field #number wire Wire caught by player when trapped. --- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. +-- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elements are of type @{#AIRBOSS.GrooveData}. -- @field #table points Points of passes until finally landed. -- @field #number finalscore Final score if points are averaged over multiple passes. -- @field #boolean valid If true, player made a valid approach. Is set true on start of Groove X. +-- @field #boolean subtitles If true, display subtitles of radio messages. -- @extends #AIRBOSS.FlightGroup ---- Main radio menu. +--- Main radio menu: F10 Other/Airboss -- @field #table MenuF10 AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.8.1" +AIRBOSS.version="0.9.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Allow up to two flights per Case I marshal stack. --- TODO: Add max stack for Case I and define waiting queue outside CCZ. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! --- TODO: Maybe do an additional step at the initial (Case II) or bullseye (Case III) and register player in case he missed some steps. --- TODO: What happens when section lead or member dies? -- TODO: Player eject and crash debrief "gradings". --- TODO: Subtitles off options on player level. --- TODO: PWO during case 2/3. Also when too close to other player. +-- TODO: What happens when section lead or member dies? +-- TODO: PWO during case 2/3. +-- TODO: PWO when player comes too close to other flight. -- TODO: Option to filter AI groups for recovery. +-- DONE: Rework radio messages. Better control over player board numbers. +-- DONE: Case I & II/III zone so that player gets into pattern automatically. Case I 3 position on the circle. Case II/III when the player enters the approach corridor maybe? +-- DONE: Add static weather information. +-- DONE: Allow up to two flights per Case I marshal stack. +-- DONE: Add max stack for Case I and define waiting queue outside CCZ. +-- DONE: Maybe do an additional step at the initial (Case II) or bullseye (Case III) and register player in case he missed some steps. +-- DONE: Subtitles off options on player level. -- DONE: Persistence of results. -- DONE: Foul deck waveoff. -- DONE: Get Charlie time estimate function. @@ -1348,12 +1418,12 @@ AIRBOSS.version="0.8.1" -- DONE: Improve radio messages. Maybe usersound for messages which are only meant for players? -- DONE: Add voice over fly needs and welcome aboard. -- DONE: Improve trapped wire calculation. --- DONE: Carrier zone with dimensions of carrier. to check if landing happend on deck. +-- DONE: Carrier zone with dimensions of carrier. to check if landing happened on deck. -- DONE: Carrier runway zone for fould deck check. -- DONE: More Hints for Case II/III. -- DONE: Set magnetic declination function. -- DONE: First send AI to marshal and then allow them into the landing pattern ==> task function when reaching the waypoint. --- DONE: Extract (static) weather from mission for cloud covery etc. +-- DONE: Extract (static) weather from mission for cloud cover etc. -- DONE: Check distance to players during approach. -- DONE: Option to turn AI handling off. -- DONE: Add user functions. @@ -1441,6 +1511,9 @@ function AIRBOSS:New(carriername, alias) self.LSORadio:SetAlias("LSO") self:SetLSORadio() + -- Set LSO call interval. Default 4 sec. + self:SetLSOCallInterval() + -- Radio scheduler. self.radiotimer=SCHEDULER:New() @@ -1453,17 +1526,35 @@ function AIRBOSS:New(carriername, alias) -- Set TACAN to channel 74X. self:SetTACAN() - -- Set max aircraft in landing pattern. + -- Set max aircraft in landing pattern. Default 4. self:SetMaxLandingPattern() + -- Set max Case I Marshal stacks. Default 3. + self:SetMaxMarshalStacks() + + -- Set max section members. Default 2. + self:SetMaxSectionSize(2) + + -- Set max flights per stack. Default is 2. + self:SetMaxFlightsPerStack(2) + -- Set AI handling On. self:SetHandleAION() - -- Default recovery case. This sets self.defaultcase and self.case. - self:SetRecoveryCase(1) + -- Airboss is a nice guy. + self:SetAirbossNiceGuy() + + -- Mission uses static weather by default. + self:SetStaticWeather() + + -- Default recovery case. This sets self.defaultcase and self.case. Default Case I. + self:SetRecoveryCase() -- Set holding offset to 0 degrees. This set self.defaultoffset and self.holdingoffset. self:SetHoldingOffsetAngle() + + -- Set Marshal stack radius. Default 2.75 NM, which gives a diameter of 5.5 NM. + self:SetMarshalRadius() -- Default player skill EASY. self:SetDefaultPlayerSkill(AIRBOSS.Difficulty.EASY) @@ -1476,12 +1567,12 @@ function AIRBOSS:New(carriername, alias) -- Carrier patrols its waypoints until the end of time. self:SetPatrolAdInfinitum(true) - + -- Set update time intervals. self:SetQueueUpdateTime() self:SetStatusUpdateTime() - -- Menu options + -- Menu options. self:SetMenuMarkZones() self:SetMenuSmokeZones() @@ -1498,20 +1589,17 @@ function AIRBOSS:New(carriername, alias) -- Kusnetsov parameters - maybe... self:_InitStennis() else - self:E(self.lid.."ERROR: Unknown carrier type!") + self:E(self.lid..string.format("ERROR: Unknown carrier type %s!", tostring(self.carriertype))) return nil end - -- CASE I/II moving zone: Zone 2.75 NM astern and 0.1 NM starboard of the carrier with a diameter of 1 NM. - --self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, UTILS.NMToMeters(0.5), {dx=-UTILS.NMToMeters(2.75), dy=UTILS.NMToMeters(0.1), relative_to_unit=true}) - ------------------- -- Debug Section -- ------------------- -- Debug trace. if false then - self.Debug=true + self.Debug=false BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) @@ -1591,7 +1679,7 @@ function AIRBOSS:New(carriername, alias) end -- Flare points every 3 seconds for 3 minutes. - SCHEDULER:New(nil, flareme, {}, 1, 3, nil, 180) + SCHEDULER:New(nil, flareme, {}, 1, 3, nil, 180) end -- If calls should be part of self and individual for different carriers. @@ -1641,6 +1729,8 @@ function AIRBOSS:New(carriername, alias) self:AddTransition("*", "Idle", "Idle") -- Carrier is idling. self:AddTransition("Idle", "RecoveryStart", "Recovering") -- Start recovering aircraft. self:AddTransition("Recovering", "RecoveryStop", "Idle") -- Stop recovering aircraft. + self:AddTransition("Recovering", "RecoveryPause", "Paused") -- Pause recovering aircraft. + self:AddTransition("Paused", "RecoveryUnpause", "Recovering") -- Unpause recovering aircraft. self:AddTransition("*", "Status", "*") -- Update status of players and queues. self:AddTransition("*", "RecoveryCase", "*") -- Switch to another case recovery. self:AddTransition("*", "Save", "*") -- Save player scores to file. @@ -1675,11 +1765,19 @@ function AIRBOSS:New(carriername, alias) --- Triggers the FSM delayed event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern. -- @function [parent=#AIRBOSS] __RecoveryStart - -- @param #number delay Delay in seconds. -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. -- @param #number Case Recovery case (1, 2 or 3) that is started. -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + --- On after "RecoveryStart" user function. Called when recovery of aircraft is started and carrier switches to state "Recovering". + -- @function [parent=#AIRBOSS] OnAfterRecoveryStart + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Case The recovery case (1, 2 or 3) to start. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. --- Triggers the FSM event "RecoveryStop" that stops the recovery of aircraft. -- @function [parent=#AIRBOSS] RecoveryStop @@ -1691,6 +1789,27 @@ function AIRBOSS:New(carriername, alias) -- @param #number delay Delay in seconds. + --- Triggers the FSM event "RecoveryPause" that pauses the recovery of aircraft. + -- @function [parent=#AIRBOSS] RecoveryPause + -- @param #AIRBOSS self + -- @param #number duration Duration of pause in seconds. After that recovery is automatically resumed. + + --- Triggers the FSM delayed event "RecoveryPause" that pauses the recovery of aircraft. + -- @function [parent=#AIRBOSS] __RecoveryPause + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number duration Duration of pause in seconds. After that recovery is automatically resumed. + + --- Triggers the FSM event "RecoveryUnpause" that resumes the recovery of aircraft if it was paused. + -- @function [parent=#AIRBOSS] RecoveryUnpause + -- @param #AIRBOSS self + + --- Triggers the FSM delayed event "RecoveryUnpause" that resumes the recovery of aircraft if it was paused. + -- @function [parent=#AIRBOSS] __RecoveryUnpause + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "RecoveryCase" that switches the aircraft recovery case. -- @function [parent=#AIRBOSS] RecoveryCase -- @param #AIRBOSS self @@ -1709,14 +1828,14 @@ function AIRBOSS:New(carriername, alias) -- @function [parent=#AIRBOSS] Save -- @param #AIRBOSS self -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. - -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. --- Triggers the FSM delayed event "Save" that saved the player scores to a file. -- @function [parent=#AIRBOSS] __Save -- @param #AIRBOSS self -- @param #number delay Delay in seconds. -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. - -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. --- On after "Save" event user function. Called when the player scores are saved to disk. -- @function [parent=#AIRBOSS] OnAfterSave @@ -1725,7 +1844,7 @@ function AIRBOSS:New(carriername, alias) -- @param #string Event Event. -- @param #string To To state. -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. - -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. --- Triggers the FSM event "Load" that loads the player scores from a file. AIRBOSS FSM must **not** be started at this point. @@ -1739,7 +1858,7 @@ function AIRBOSS:New(carriername, alias) -- @param #AIRBOSS self -- @param #number delay Delay in seconds. -- @param #string path Path where the file is located. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. - -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. --- On after "Load" event user function. Called when the player scores are loaded from disk. -- @function [parent=#AIRBOSS] OnAfterLoad @@ -1748,7 +1867,7 @@ function AIRBOSS:New(carriername, alias) -- @param #string Event Event. -- @param #string To To state. -- @param #string path Path where the file is located. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. - -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. @@ -1833,7 +1952,7 @@ end -- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. -- @param #number case Recovery case for that time slot. Number between one and three. -- @param #number holdingoffset Only for CASE II/III: Angle in degrees the holding pattern is offset. --- @return #AIRBOSS self +-- @return #AIRBOSS.Recovery Recovery window. function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset) -- Absolute mission time in seconds. @@ -1850,11 +1969,11 @@ function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset) -- Consistancy check for timing. if Tstart>Tstop then - self:E(string.format("ERROR: Recovery stop time %s lies before recovery start time %s! Recovery windows rejected.", UTILS.SecondsToClock(Tstart), UTILS.SecondsToClock(Tstop))) + self:E(string.format("ERROR: Recovery stop time %s lies before recovery start time %s! Recovery window rejected.", UTILS.SecondsToClock(Tstart), UTILS.SecondsToClock(Tstop))) return self end if Tstop<=Tnow then - self:I(string.format("WARNING: Recovery stop time %s already over. Tnow=%s! Recovery windows rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow))) + self:I(string.format("WARNING: Recovery stop time %s already over. Tnow=%s! Recovery window rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow))) return self end @@ -1867,7 +1986,10 @@ function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset) -- Offset zero for case I. if case==1 then holdingoffset=0 - end + end + + -- Increase counter. + self.windowcount=self.windowcount+1 -- Recovery window. local recovery={} --#AIRBOSS.Recovery @@ -1877,19 +1999,118 @@ function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset) recovery.OFFSET=holdingoffset recovery.OPEN=false recovery.OVER=false + recovery.ID=self.windowcount -- Add to table table.insert(self.recoverytimes, recovery) + return recovery +end + +--- Close currently running recovery window and stop recovery ops. Recovery window is deleted. +-- @param #AIRBOSS self +-- @param #number delay (Optional) Delay in seconds before the window is deleted. +function AIRBOSS:CloseCurrentRecoveryWindow(delay) + + if delay and delay>0 then + SCHEDULER:New(nil, self.CloseCurrentRecoveryWindow, {self}, delay) + else + if self:IsRecovering() and self.recoverywindow and self.recoverywindow.OPEN then + self:RecoveryStop() + self.recoverywindow.OPEN=false + self.recoverywindow.OVER=true + self:DeleteRecoveryWindow(self.recoverywindow) + end + end +end + +--- Delete all recovery windows. +-- @param #AIRBOSS self +-- @param #number delay (Optional) Delay in seconds before the windows are deleted. +-- @return #AIRBOSS self +function AIRBOSS:DeleteAllRecoveryWindows(delay) + + -- Loop over all recovery windows. + for _,recovery in pairs(self.recoverytimes) do + self:DeleteRecoveryWindow(recovery, delay) + end + return self end +--- Return the recovery window of the given ID. +-- @param #AIRBOSS self +-- @param #number id The ID of the recovery window. +-- @return #AIRBOSS.Recovery Recovery window with the right ID or nil if no such window exists. +function AIRBOSS:GetRecoveryWindowByID(id) + if id then + for _,_window in pairs(self.recoverytimes) do + local window=_window --#AIRBOSS.Recovery + if window.ID==id then + return window + end + end + end + return nil +end + +--- Delete a recovery window. If the window is currently open, it is closed and the recovery stopped. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Recovery window Recovery window. +-- @param #number delay Delay in seconds, before the window is deleted. +function AIRBOSS:DeleteRecoveryWindow(window, delay) + + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(nil, self.DeleteRecoveryWindow, {self, window}, delay) + else + + for i,_recovery in pairs(self.recoverytimes) do + local recovery=_recovery --#AIRBOSS.Recovery + + if window and window.ID==recovery.ID then + if window.OPEN then + -- Window is currently open. + self:RecoveryStop() + --self:CloseCurrentRecoveryWindow() + else + table.remove(self.recoverytimes, i) + end + + end + end + end +end + --- Set time interval for updating queues and other stuff. -- @param #AIRBOSS self -- @param #number interval Time interval in seconds. Default 30 sec. -- @return #AIRBOSS self function AIRBOSS:SetQueueUpdateTime(interval) self.dTqueue=interval or 30 + return self +end + +--- Set time interval between LSO calls. Optimal time in the groove is ~16 seconds. So the default of 4 seconds gives around 3-4 correction calls in the groove. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds between LSO calls. Default 4 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetLSOCallInterval(timeinterval) + self.LSOdT=timeinterval or 4 + return self +end + +--- Airboss is a rather nice guy and not strictly following the rules. Fore example, he does allow you into the landing pattern if you are not coming from the Marshal stack. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, Airboss bends the rules a bit. +-- @return #AIRBOSS self +function AIRBOSS:SetAirbossNiceGuy(switch) + if switch==true or switch==nil then + self.airbossnice=true + else + self.airbossnice=false + end + return self end --- Set time interval for updating player status and other things. @@ -1898,13 +2119,17 @@ end -- @return #AIRBOSS self function AIRBOSS:SetStatusUpdateTime(interval) self.dTstatus=interval or 0.5 + return self end ---- Disable automatic TACAN activation +--- Set Case I Marshal radius. This is the radius of the valid zone around "the post" aircraft are supposed to be holding in the Case I Marshal stack. +-- The post is 2.5 NM port of the carrier. -- @param #AIRBOSS self +-- @param #number Radius in NM. Default 2.75 NM, which gives a diameter of 5.5 NM. -- @return #AIRBOSS self -function AIRBOSS:SetTACANoff() - self.TACANon=false +function AIRBOSS:SetMarshalRadius(radius) + self.marshalradius=UTILS.NMToMeters(radius or 2.75) + return self end --- Enable or disable F10 radio menu for marking zones via smoke or flares. @@ -1917,6 +2142,7 @@ function AIRBOSS:SetMenuMarkZones(switch) else self.menumarkzones=false end + return self end --- Enable or disable F10 radio menu for marking zones via smoke. @@ -1929,6 +2155,29 @@ function AIRBOSS:SetMenuSmokeZones(switch) else self.menusmokezones=false end + return self +end + +--- Specify weather the mission has set static or dynamic weather. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, mission uses static weather. If false, dynamic weather is used in this mission. +-- @return #AIRBOSS self +function AIRBOSS:SetStaticWeather(switch) + if switch==nil or switch==true then + self.staticweather=true + else + self.staticweather=false + end + return self +end + + +--- Disable automatic TACAN activation +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetTACANoff() + self.TACANon=false + return self end --- Set TACAN channel of carrier. @@ -1952,6 +2201,7 @@ end -- @return #AIRBOSS self function AIRBOSS:SetICLSoff() self.ICLSon=false + return self end --- Set ICLS channel of carrier. @@ -2018,17 +2268,57 @@ end -- @return #AIRBOSS self function AIRBOSS:SetUserSoundRadio() self.usersoundradio=true -end - ---- Set number of aircraft units which can be in the landing pattern before the pattern is full. --- @param #AIRBOSS self --- @param #number nmax Max number. Default 4. --- @return #AIRBOSS self -function AIRBOSS:SetMaxLandingPattern(nmax) - self.Nmaxpattern=nmax or 4 return self end +--- Set number of aircraft units, which can be in the landing pattern before the pattern is full. +-- @param #AIRBOSS self +-- @param #number nmax Max number. Default 4. Minimum is 1, maximum is 6. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxLandingPattern(nmax) + nmax=nmax or 4 + nmax=math.max(nmax,1) + nmax=math.min(nmax,6) + self.Nmaxpattern=nmax + return self +end + +--- Set number available Case I Marshal stacks. If Marshal stacks are full, flights requesting Marshal will be told to hold outside 10 NM zone until a stack becomes available again. +-- Marshal stacks for Case II/III are unlimited. +-- @param #AIRBOSS self +-- @param #number nmax Max number of stacks available to players and AI flights. Default 3, i.e. angels 2, 3, 4. Minimum is 1. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxMarshalStacks(nmax) + self.Nmaxmarshal=nmax or 3 + self.Nmaxmarshal=math.max(self.Nmaxmarshal, 1) + return self +end + +--- Set max number of section members. Minimum is one, i.e. the section lead itself. Maximum number is four. Default is two, i.e. the lead and one other human flight. +-- @param #AIRBOSS self +-- @param #number nmax Number of max allowed members including the lead itself. For example, Nmax=2 means a section lead plus one member. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxSectionSize(nmax) + nmax=nmax or 2 + nmax=math.max(nmax,1) + nmax=math.min(nmax,4) + self.NmaxSection=nmax-1 -- We substract one because internally the section lead is not counted! + return self +end + +--- Set max number of flights per stack. All members of a section count as one "flight". +-- @param #AIRBOSS self +-- @param #number nmax Number of max allowed flights per stack. Default is two. Minimum is one, maximum is 4. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxFlightsPerStack(nmax) + nmax=nmax or 2 + nmax=math.max(nmax,1) + nmax=math.min(nmax,4) + self.NmaxStack=nmax + return self +end + + --- Handle AI aircraft. -- @param #AIRBOSS self -- @return #AIRBOSS self @@ -2132,9 +2422,7 @@ end -- @param #number declination Declination in degrees or nil for default declination of the map. -- @return #AIRBOSS self function AIRBOSS:SetMagneticDeclination(declination) - self.magvar=declination or UTILS.GetMagneticDeclination() - return self end @@ -2160,6 +2448,13 @@ function AIRBOSS:IsIdle() return self:is("Idle") end +--- Check if recovery of aircraft is paused. +-- @param #AIRBOSS self +-- @return #boolean If true, recovery is paused +function AIRBOSS:IsPaused() + return self:is("Paused") +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM event functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2316,13 +2611,13 @@ function AIRBOSS:_CheckAIStatus() -- Pilot: "405, Hornet Ball, 3.2" -- TODO: Voice over. local text=string.format("%s Ball, %.1f.", self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) - self:MessageToPattern(text, element.onboard, "", 3, false, 0, true) + self:MessageToPattern(text, element.onboard, "", 3) -- Debug message. MESSAGE:New(string.format("%s, %s", element.onboard, text), 15, "DEBUG"):ToAllIf(self.Debug) -- Paddles: Roger ball after 3 seconds. - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 3) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 6) -- Flight element called the ball. element.ballcall=true @@ -2443,7 +2738,7 @@ function AIRBOSS:_CheckRecoveryTimes() -- Handle case with no recoveries. if #self.recoverytimes==0 then - text=" none!" + text=text.." none!" end -- Sort windows wrt to start time. @@ -2470,7 +2765,7 @@ function AIRBOSS:_CheckRecoveryTimes() -- Start time has passed. if time %d", self.case, Case) if Case>1 then text=text..string.format(" Holding offset angle %d degrees.", Offset) end @@ -2580,6 +2920,30 @@ function AIRBOSS:onafterRecoveryCase(From, Event, To, Case, Offset) -- Set holding offset. self.holdingoffset=Offset + + -- Update case of all flights not in Marshal or Pattern queue. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + if not (self:_InQueue(self.Qmarshal, flight.group) or self:_InQueue(self.Qpattern, flight.group)) then + + -- Also not for section members. These are not in the marshal or pattern queue if the lead is. + if flight.name~=flight.seclead then + local lead=self.players[flight.seclead] + + if lead and not (self:_InQueue(self.Qmarshal, lead.group) or self:_InQueue(self.Qpattern, lead.group)) then + -- This is section member and the lead is not in the Marshal or Pattern queue. + flight.case=self.case + end + + else + + -- This is a flight without section or the section lead. + flight.case=self.case + + end + + end + end end --- On after "RecoveryStart" event. Recovery of aircraft is started and carrier switches to state "Recovering". @@ -2598,31 +2962,91 @@ function AIRBOSS:onafterRecoveryStart(From, Event, To, Case, Offset) Offset=Offset or self.defaultoffset -- Debug output. - local text=string.format("Starting aircraft recovery case %d.", Case) + local text=string.format("Starting aircraft recovery Case %d ops.", Case) if Case>1 then - text=text..string.format(" Holding offset angle %d degrees.", Offset) + local radial=self:GetRadial(Case, true, true, true) + text=text..string.format(" Marshal radial %03d°.", radial) end MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) self:T(self.lid..text) - -- Message to all players in marshal stack. - -- TODO: maybe to all flights in CCA? - self:MessageToMarshal(text, "MARSHAL", "99") + -- Message to all players inside CCA. + self:MessageToMarshal(text, "AIRBOSS", "99") -- Switch to case. self:RecoveryCase(Case, Offset) end ---- On after "RecoveryStop" event. Recovery of aircraft is stopped and carrier switches to state "Idle". +--- On after "RecoveryStop" event. Recovery of aircraft is stopped and carrier switches to state "Idle". Running recovery window is deleted. -- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function AIRBOSS:onafterRecoveryStop(From, Event, To) -- Debug output. - self:T(self.lid..string.format("Stopping aircraft recovery. Carrier goes to state idle.")) + self:T(self.lid..string.format("Stopping aircraft recovery.")) + + -- Delete current recovery window if open. + if self.recoverywindow and self.recoverywindow.OPEN==true then + self.recoverywindow.OPEN=false + self.recoverywindow.OVER=true + self:DeleteRecoveryWindow(self.recoverywindow) + end + + -- Message text. + local text=string.format("Case %d recovery ops are stopped.", self.case) + + -- Message to Marshal. + self:MessageToMarshal(text, "AIRBOSS", "99") + + -- Check recovery windows. This sets self.recoverywindow to the next window. + self:_CheckRecoveryTimes() end + +--- On after "RecoveryPause" event. Recovery of aircraft is paused. Marshal queue stays intact. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number duration Duration of pause in seconds. After that recovery is resumed automatically. +function AIRBOSS:onafterRecoveryPause(From, Event, To, duration) + -- Debug output. + self:T(self.lid..string.format("Pausing aircraft recovery.")) + + -- Message text + local text=string.format("aircraft recovery is paused until further notice.") + if duration then + + -- Auto resume. + self:__RecoveryUnpause(duration) + + -- Message text. + local clock=UTILS.SecondsToClock(timer.getAbsTime()+duration) + text=string.format("aircraft recovery is paused and will be resumed at %s.", clock) + end + + -- Message to Marshal. + self:MessageToMarshal(text, "AIRBOSS", "99") +end + +--- On after "RecoveryUnpause" event. Recovery of aircraft is resumed. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterRecoveryUnpause(From, Event, To) + -- Debug output. + self:T(self.lid..string.format("Unpausing aircraft recovery.")) + + -- Message text. + local text=string.format("resuming aircraft recovery.") + + -- Message to Marshal. + self:MessageToMarshal(text, "AIRBOSS", "99") +end + + --- On after "Idle" event. Carrier goes to state "Idle". -- @param #AIRBOSS self -- @param #string From From state. @@ -3242,13 +3666,9 @@ function AIRBOSS:_GetNextMarshalFight() local Tmarshal=timer.getAbsTime()-flight.time -- Min time in marshal stack. - local TmarshalMin=2*60 - if flight.ai then - -- Three minutes for AI. - TmarshalMin=3*60 - else - -- Two minutes for human pilots. - TmarshalMin=2*60 + local TmarshalMin=2*60 --Two minutes for human players. + if flight.ai then + TmarshalMin=3*60 -- Three minutes for AI. end -- Check if conditions are right. @@ -3266,17 +3686,89 @@ end function AIRBOSS:_CheckQueue() -- Print queues. - self:_PrintQueue(self.flights, "All Flights") + if self.Debug then + self:_PrintQueue(self.flights, "All Flights") + end self:_PrintQueue(self.Qmarshal, "Marshal") self:_PrintQueue(self.Qpattern, "Pattern") + self:_PrintQueue(self.Qwaiting, "Waiting") - -- Get number of aircraft units(!) currently in pattern. + -- If flights are waiting outside 10 NM zone and carrier switches from Case I to Case II/III, they should be added to the Marshal stack as now there is no stack limit any more. + if self.case>1 then + for _,_flight in pairs(self.Qwaiting) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Remove flight from waiting queue. + local removed=self:_RemoveFlightFromQueue(self.Qwaiting, flight) + + if removed then + + -- Get free stack + local stack=self:_GetFreeStack(flight.ai) + + -- Debug info. + self:T(self.lid..string.format("Moving flight %s onboard %s from Waiting queue to Case %d Marshal stack %d", flight.groupname, flight.onboard, self.case, stack)) + + -- Send flight to marshal stack. + if flight.ai then + self:_MarshalAI(flight, stack) + else + self:_MarshalPlayer(flight, stack) + end + + -- Break the loop so that only one flight per 30 seconds is removed. + break + end + + end + end + + -- Check if carrier is currently in recovery mode. + if not self:IsRecovering() then + + -- Loop over all flights currently in the marshal queue. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Check if they have the right case. + if flight.case~=self.case then + + -- Remove flight from marshal queue. + local removed=self:_RemoveFlightFromQueue(self.Qmarshal, flight) + + if removed then + + -- Get free stack + local stack=self:_GetFreeStack(flight.ai) + + -- Debug output. + self:T(self.lid..string.format("Moving flight %s onboard %s from Marshal Case %d ==> %d Marshal stack %d", flight.groupname, flight.onboard, flight.case, self.case, stack)) + + -- Send flight to marshal queue. + if flight.ai then + self:_MarshalAI(flight, stack) + else + self:_MarshalPlayer(flight, stack) + end + + -- Break the loop so that only one flight per 30 seconds is removed. No spam of messages, no conflict with the loop over queue entries. + break + end + end + end + + + -- Not recovering ==> skip the rest! + return + end + + -- Get number of airborne aircraft units(!) currently in pattern. local _,npattern=self:_GetQueueInfo(self.Qpattern) -- Get next marshal flight. local marshalflight=self:_GetNextMarshalFight() - -- Check if there are flights in marshal strack and if the pattern is free. + -- Check if there are flights waiting in the Marshal stack and if the pattern is free. if marshalflight and npatternTpatternMin then + -- Check interval to last pattern flight. + if Tpattern>TpatternMin then self:T(self.lid..string.format("Sending marshal flight %s to pattern.", marshalflight.groupname)) - self:_CheckCollapseMarshalStack(marshalflight) + self:_ClearForLanding(marshalflight) end end end +--- Clear flight for landing. AI are removed from Marshal queue and the Marshal stack is collapsed. +-- If next in line is an AI flight, this is done. If human player is next, we wait for "Commence" via F10 radio menu command. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight to go to pattern. +function AIRBOSS:_ClearForLanding(flight) + + -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence. + if flight.ai then + + -- Collapse stack and send AI to pattern. + self:_RemoveFlightFromMarshalQueue(flight, false) + self:_LandAI(flight) + + else + + -- Set step to commencing. This will trigger the zone check until the player is in the right place. + self:_SetPlayerStep(flight, AIRBOSS.PatternStep.COMMENCING, 3) + + end + + -- Inform all Marshal flights. + local text=string.format("you are cleared for Case %d recovery.", flight.case) + + -- Add a little delay because message that recovery window opened could come just before. + self:MessageToMarshal(text, "MARSHAL", flight.onboard, 10, false, 2) + +end + +--- Set player step. Any warning is erased and next step hint shown. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step Next step. +-- @param #number delay (Optional) Set set after a delay in seconds. +function AIRBOSS:_SetPlayerStep(playerData, step, delay) + + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(nil, self._SetPlayerStep, {self, playerData, step}, delay) + else + + -- Check if player still exists after possible delay. + if playerData then + + -- Set player step. + playerData.step=step + + -- Erase warning. + playerData.warning=nil + + -- Next step hint. + self:_StepHint(playerData) + end + + end + +end + --- Scan carrier zone for (new) units. -- @param #AIRBOSS self function AIRBOSS:_ScanCarrierZone() @@ -3328,7 +3877,6 @@ function AIRBOSS:_ScanCarrierZone() local coord=self:GetCoordinate() -- Scan radius = radius of the CCA. - --local Rout=UTILS.NMToMeters(50) local RCCZ=self.zoneCCA:GetRadius() -- Debug info. @@ -3402,13 +3950,24 @@ function AIRBOSS:_ScanCarrierZone() else -- Get the next free stack for current recovery case. - local stack=self:_GetFreeStack(self.case) + local stack=self:_GetFreeStack(knownflight.ai) - -- Send AI to marshal stack. - self:_MarshalAI(knownflight, stack) + if stack then - -- Add group to marshal stack queue. - self:_AddMarshalGroup(knownflight, stack) + -- Send AI to marshal stack. + self:_MarshalAI(knownflight, stack) + + else + + -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available. + if not self:_InQueue(self.Qwaiting, knownflight.group) then + self:_WaitAI(knownflight) + end + + end + + -- Break the loop to not have all flights at once! Spams the message screen. + break end -- Tanker end -- Closed in @@ -3432,7 +3991,6 @@ function AIRBOSS:_ScanCarrierZone() local flight=_flight --#AIRBOSS.FlightGroup if insideCCA[flight.groupname]==nil then -- Do not remove flights in marshal pattern. At least for case 2 & 3. If zone is set small, they might be outside in the holding pattern. - -- TODO: remove only AI flights if flight.ai and not (self:_InQueue(self.Qmarshal, flight.group) or self:_InQueue(self.Qpattern, flight.group)) then table.insert(remove, flight) end @@ -3446,20 +4004,65 @@ function AIRBOSS:_ScanCarrierZone() end - ---- Orbit at a specified position at a specified alititude with a specified speed. +--- Tell player to wait outside the 10 NM zone until a Marshal stack is available. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_MarshalPlayer(playerData) +function AIRBOSS:_WaitPlayer(playerData) -- Check if flight is known to the airboss already. if playerData then - -- Get free stack. - local mystack=self:_GetFreeStack(self.case) + -- Number of waiting flights + local nwaiting=#self.Qwaiting + + -- Message text. + local text=string.format("Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions. ") + if nwaiting==1 then + text=text..string.format("There is one flight ahead of you.") + elseif nwaiting>1 then + text=text..string.format("There are %d flights ahead of you.", nwaiting) + else + text=text..string.format("You are next in line.") + end + -- Send message. + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 10) + + -- Add player flight to waiting queue. + table.insert(self.Qwaiting, playerData) + + -- Set time stamp. + playerData.time=timer.getAbsTime() + + -- Set step to waiting. + playerData.step=AIRBOSS.PatternStep.WAITING + playerData.warning=nil + + -- Set all flights in section to waiting. + for _,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + flight.step=AIRBOSS.PatternStep.WAITING + flight.time=timer.getAbsTime() + flight.warning=nil + self:MessageToPlayer(flight, text, "AIRBOSS", nil, 10) + end + + end + +end + + +--- Orbit at a specified position at a specified altitude with a specified speed. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number stack The Marshal stack the player gets. +function AIRBOSS:_MarshalPlayer(playerData, stack) + + -- Check if flight is known to the airboss already. + if playerData then + -- Add group to marshal stack. - self:_AddMarshalGroup(playerData, mystack) + self:_AddMarshalGroup(playerData, stack) -- Set step to holding. playerData.step=AIRBOSS.PatternStep.HOLDING @@ -3470,36 +4073,111 @@ function AIRBOSS:_MarshalPlayer(playerData) -- Set same stack for all flights in section. for _,_flight in pairs(playerData.section) do + -- TODO: inform section members. local flight=_flight --#AIRBOSS.PlayerData + flight.case=playerData.case flight.step=AIRBOSS.PatternStep.HOLDING flight.holding=nil - flight.flag:Set(mystack) + flight.flag:Set(stack) end else - - -- Flight is not registered yet. - local text="you are not yet registered inside the CCA. Marshal request denied!" - self:MessageToPlayer(playerData, text, "MARSHAL") - + self:E(self.lid.."ERROR: Could not add player to Marshal stack! playerData=nil") end end ---- Command AI flight to orbit at a specified position at a specified alititude with a specified speed. +--- Command AI flight to orbit outside the 10 NM zone and wait for a free Marshal stack. -- If the flight is not already holding in the Marshal stack, it is guided there first. -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight group. --- @param #number nstack Stack number of group. This should be #self.Qmarshal+1 for new flight groups. -function AIRBOSS:_MarshalAI(flight, nstack) +function AIRBOSS:_WaitAI(flight) + + -- Set flag to something other than -100 and <0 + flight.flag:Set(-99) + + -- Add AI flight to waiting queue. + table.insert(self.Qwaiting, flight) -- Flight group name. local group=flight.group local groupname=flight.groupname + + -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) + local speedOrbitMps=UTILS.KnotsToMps(274) + -- Orbit speed in km/h for waypoints. + local speedOrbitKmh=UTILS.KnotsToKmph(274) + + -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) + local speedTransit=UTILS.KnotsToKmph(370) + + -- Carrier coordinate + local cv=self:GetCoordinate() + + -- Coordinate of flight group + local fc=group:GetCoordinate() + + -- Carrier heading + local hdg=self:GetHeading(false) + + -- Heading from carrier to flight group + local hdgto=cv:HeadingTo(fc) + + -- Holding alitude between angels 6 and 10 (random). + local angels=math.random(6,10) + local altitude=UTILS.FeetToMeters(angels*1000) + + -- Point outsize 10 NM zone of the carrier. + local p0=cv:Translate(UTILS.NMToMeters(11), hdgto):Translate(UTILS.NMToMeters(5), hdg):SetAltitude(altitude) + + -- Waypoints array to be filled depending on case etc. + local wp={} + + -- Current position. Always good for as the first waypoint. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") + + -- Set orbit task. + local taskorbit=group:TaskOrbit(p0, altitude, speedOrbitMps) + + -- Orbit at waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedOrbitKmh, {taskorbit}, string.format("Waiting Orbit at Angels %d", angels)) + + -- Debug markers. + if self.Debug then + p0:MarkToAll(string.format("Waiting Orbit of flight %s at Angels %s", groupname, angels)) + end + + -- Reinit waypoints. + group:WayPointInitialize(wp) + + -- Route group. + group:Route(wp, 0) +end + +--- Command AI flight to orbit at a specified position at a specified altitude with a specified speed. If flight is not in the Marshal queue yet, it is added. This fixes the recovery case. +-- If the flight is not already holding in the Marshal stack, it is guided there first. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @param #number nstack Stack number of group. Can also be the current stack if AI position needs to be updated wrt to changed carrier position. +function AIRBOSS:_MarshalAI(flight, nstack) + + -- Check if flight is already in Marshal queue. + if not self:_InQueue(self.Qmarshal,flight.group) then + -- Add group to marshal stack queue. + self:_AddMarshalGroup(flight, nstack) + end + + -- Recovery case. + local case=flight.case + -- Get old/current stack. local ostack=flight.flag:Get() - + + -- Flight group name. + local group=flight.group + local groupname=flight.groupname + -- Set new stack. flight.flag:Set(nstack) @@ -3508,9 +4186,6 @@ function AIRBOSS:_MarshalAI(flight, nstack) -- Carrier heading. local hdg=self:GetHeading() - - -- Recovery case. - local case=flight.case -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) local speedOrbitMps=UTILS.KnotsToMps(274) @@ -3519,7 +4194,7 @@ function AIRBOSS:_MarshalAI(flight, nstack) local speedOrbitKmh=UTILS.KnotsToKmph(274) -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) - local speedTransit=UTILS.KnotsToKmph(400) + local speedTransit=UTILS.KnotsToKmph(370) local altitude local p0 --Core.Point#COORDINATE @@ -3580,7 +4255,7 @@ function AIRBOSS:_MarshalAI(flight, nstack) -- In Marshal Pattern -- ------------------------ - -- Debug info. + -- Debug info. self:T(self.lid..string.format("Updating AI flight %s at marshal stack %d-->%d.", groupname, ostack, nstack)) -- Current position. Speed expected in km/h. @@ -3630,7 +4305,7 @@ function AIRBOSS:_LandAI(flight) Speed=UTILS.KnotsToKmph(200) elseif flight.actype==AIRBOSS.AircraftCarrier.E2D then Speed=UTILS.KnotsToKmph(150) - elseif flight.actype==AIRBOSS.AircraftCarrier.F14A_AI or flight.actype==AIRBOSS.AircraftCarrier.F14B or flight.actype==AIRBOSS.AircraftCarrier.F14B then + elseif flight.actype==AIRBOSS.AircraftCarrier.F14A_AI or flight.actype==AIRBOSS.AircraftCarrier.F14A or flight.actype==AIRBOSS.AircraftCarrier.F14B then Speed=UTILS.KnotsToKmph(175) elseif flight.actype==AIRBOSS.AircraftCarrier.S3B or flight.actype==AIRBOSS.AircraftCarrier.S3BTANKER then Speed=UTILS.KnotsToKmph(140) @@ -3716,7 +4391,7 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. angels0=6 - -- Distance: d=n*angles0+15 NM, so first stack is at 15+6=21 NM + -- Distance: d=n*angels0+15 NM, so first stack is at 15+6=21 NM Dist=UTILS.NMToMeters(nstack+angels0+15) -- Get correct radial depending on recovery case including offset. @@ -3841,7 +4516,7 @@ function AIRBOSS:_GetCharlieTime(flightgroup) return Tcharlie end ---- Add a flight group to a specific marshal stack and to the marshal queue. +--- Add a flight group to the Marshal queue at a specific stack. Flight is informed via message. This fixes the recovery case to the current case ops in progress self.case). -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight group. -- @param #number stack Marshal stack. This (re-)sets the flag value. @@ -3873,6 +4548,9 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) local text=string.format("Case %d, BRC is %03d°, hold at %d. Expected Charlie Time %s.\n", flight.case, brc, alt, tostring(Ccharlie)) text=text..string.format("Altimeter %.2f. Report see me.", P) + -- Message to all players. + self:MessageToMarshal(text, "MARSHAL", flight.onboard) + -- Hint about TACAN bearing. if self.TACANon and (not flight.ai) and flight.difficulty==AIRBOSS.Difficulty.EASY then -- Get inverse magnetic radial potential offset. @@ -3881,41 +4559,10 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) -- For case 1 we want the BRC but above routine return FB. radial=self:GetBRC() end - text=text..string.format("\nSelect TACAN %03d°, channel %d%s (%s)", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + local text=string.format("Select TACAN %03d°, channel %d%s (%s)", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + self:MessageToPlayer(flight, text, nil, "") end - - -- Message to all players. - self:MessageToAll(text, "MARSHAL", flight.onboard) -end - ---- Check if marshal stack can be collapsed. --- If next in line is an AI flight, this is done. If human player is next, we wait for "Commence" via F10 radio menu command. --- @param #AIRBOSS self --- @param #AIRBOSS.FlightGroup flight Flight to go to pattern. -function AIRBOSS:_CheckCollapseMarshalStack(flight) - - -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence. - if flight.ai then - -- Collapse stack and send AI to pattern. - --self:_CollapseMarshalStack(flight) - self:_RemoveFlightFromMarshalQueue(flight, false) - self:_LandAI(flight) - end - - -- Inform all flights. - local text=string.format("You are cleared for Case %d recovery.", flight.case) - self:MessageToAll(text, "MARSHAL", flight.onboard) - -- Hint for human players. - if not flight.ai then - local playerData=flight --#AIRBOSS.PlayerData - - -- Hint for easy skill. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - self:MessageToPlayer(flight, string.format("Use F10 radio menu \"Request Commence\" command when ready!"), nil, "", 5) - end - end - end --- Collapse marshal stack. @@ -3944,7 +4591,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) for _,_flight in pairs(self.Qmarshal) do local mflight=_flight --#AIRBOSS.PlayerData - -- Only collaps stack of which the flight left. CASE II/III stack is the same. + -- Only collapse stack of which the flight left. CASE II/III stack is the same. if (case==1 and mflight.case==1) or (case>1 and mflight.case>1) then -- Get current flag/stack value. @@ -3953,55 +4600,65 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- Only collapse stacks above the new pattern flight. if mstack>stack then - -- New stack is old stack minus one. - -- TODO: If we include the recovery tanker, this needs to be generalized. - local newstack=mstack-1 + -- OLD: New stack is old stack minus one. + --local newstack=mstack-1 - -- Debug info. - self:T(self.lid..string.format("Collapse Marshal: Flight %s (case %d) is changing marshal stack %d --> %d.", mflight.groupname, mflight.case, mstack, newstack)) + -- NEW: Is this now right as we allow more flights per stack? + -- TODO: Question is, does the stack collapse if the lower stack is completely empty or do aircraft descent if just one flight leaves. + -- For now, assuming that the stack must be completely empty before the next higher AC are allowed to descent. + local newstack=self:_GetFreeStack(mflight.ai, mflight.case, true) - if mflight.ai then + -- Free stack has to be below. + if newstack and newstack %d.", mflight.groupname, mflight.case, mstack, newstack)) + + if mflight.ai then - else - - -- Decrease stack/flag. Human player needs to take care himself. - mflight.flag:Set(newstack) - - -- Inform players. - if mflight.difficulty~=AIRBOSS.Difficulty.HARD then + -- Command AI to decrease stack. Flag is set in the routine. + self:_MarshalAI(mflight, newstack) + + else + + -- Decrease stack/flag. Human player needs to take care himself. + mflight.flag:Set(newstack) + + -- Angels of new stack. + local angels=self:_GetAngels(self:_GetMarshalAltitude(newstack, case)) - -- Send message to all non-pros that they can descent. - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(newstack, case)) - local text=string.format("descent to next lower stack at %d ft", alt) - self:MessageToPlayer(mflight, text, "MARSHAL") + -- Inform players. + if mflight.difficulty~=AIRBOSS.Difficulty.HARD then - end - - -- Set time stamp. - mflight.time=timer.getAbsTime() - - -- Loop over section members. - for _,_sec in pairs(mflight.section) do - local sec=_sec --#AIRBOSS.PlayerData - - -- Also decrease flag for section members of flight. - sec.flag:Set(newstack) - - -- Set new time stamp. - sec.time=timer.getAbsTime() - - -- Inform section member. - if sec.difficulty~=AIRBOSS.Difficulty.HARD then - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(newstack, case)) - local text=string.format("follow your lead to next lower stack at %d ft", alt) - self:MessageToPlayer(sec, text, "MARSHAL") + -- Send message to all non-pros that they can descent. + local text=string.format("descent to stack at Angels %d.", angels) + self:MessageToPlayer(mflight, text, "MARSHAL") + end - + + -- Set time stamp. + mflight.time=timer.getAbsTime() + + -- Loop over section members. + for _,_sec in pairs(mflight.section) do + local sec=_sec --#AIRBOSS.PlayerData + + -- Also decrease flag for section members of flight. + sec.flag:Set(newstack) + + -- Set new time stamp. + sec.time=timer.getAbsTime() + + -- Inform section member. + if sec.difficulty~=AIRBOSS.Difficulty.HARD then + local text=string.format("descent to stack at Angels %d.", angels) + self:MessageToPlayer(sec, text, "MARSHAL") + end + + end + end - + end end end @@ -4012,40 +4669,38 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- Debug message. self:T(self.lid..string.format("Flight %s is leaving stack but not going to pattern.", flight.groupname)) - - -- Set flag to -1. -1 is rather arbitrary. Should not be -100 or positive. - flight.flag:Set(-1) else -- Debug message. local Tmarshal=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) self:T(self.lid..string.format("Flight %s is leaving marshal after %s and going pattern.", flight.groupname, Tmarshal)) - - -- Decrease flag. - flight.flag:Set(stack-1) - + -- Add flight to pattern queue. table.insert(self.Qpattern, flight) end + -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42). + flight.flag:Set(-1) + -- New time stamp for time in pattern. flight.time=timer.getAbsTime() - - -- Remove flight from marshal queue. - --self:_RemoveFlightFromQueue(self.Qmarshal, flight) end ---- Get next free stack depending on recovery case. Note that here we assume one flight group per stack! +--- Get next free Marshal stack. Depending on AI/human and recovery case. -- @param #AIRBOSS self +-- @param #boolean ai If true, get a free stack for an AI flight group. -- @param #number case Recovery case. Default current (self) case in progress. --- @return #number Lowest free stack available for the given case. -function AIRBOSS:_GetFreeStack(case) +-- @param #boolean empty Return lowest stack that is completely empty. +-- @return #number Lowest free stack available for the given case or nil if all Case I stacks are taken. +function AIRBOSS:_GetFreeStack(ai, case, empty) -- Recovery case. case=case or self.case + + --[[ -- Get stack local nfull @@ -4056,31 +4711,132 @@ function AIRBOSS:_GetFreeStack(case) -- Lowest Case II or III stack. nfull=self:_GetQueueInfo(self.Qmarshal, 23) end - + -- Simple case without a recovery tanker for now. local nfree=nfull+1 - --[[ - -- Get recovery tanker stack. - local tankerstack=9999 - if self.tanker and case==1 then - tankerstack=self:_GetAngels(self.tanker.altitude) - end - - if nfull so 4 free stacks. At Angels 6, we might have a tanker. + -- Next flights are asked to hold outside 10 NM zone. + if case==1 and nfree>self.Nmaxmarshal then + nfree=nil end + + return nfree + ]] - return nfree + -- New version. + + -- Max number of stacks available. + local nmaxstacks=100 + if case==1 then + nmaxstacks=self.Nmaxmarshal + end + + -- Assume up to two (human) flights per stack. All are free. + local stack={} + for i=1,nmaxstacks do + stack[i]=self.NmaxStack -- Number of human flights per stack. + end + + -- Loop over all flights in marshal stack. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Check that the case is right. + if flight.case==case then + + -- Get stack of flight. + local n=flight.flag:Get() + + if n>0 then + if flight.ai then + stack[n]=0 -- AI get one stack on their own. + else + stack[n]=stack[n]-1 + end + else + self:E(string.format("ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.", flight.groupname, n)) + end + + end + end + + -- Loop over stacks and check which one has a place left. + local nfree=nil + for i=1,nmaxstacks do + env.info(string.format("FF stack[%d]=%d", i, stack[i])) + if ai or empty then + -- AI need the whole stack. + if stack[i]==self.NmaxStack then + nfree=i + return i + end + else + -- Human players only need one free spot. + if stack[i]>0 then + nfree=i + return i + end + end + end + + return nfree end +--- Get number of (airborne) units in a flight. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight The flight group. +-- @param #boolean onground If true, include units on the ground. By default only airborne units are counted. +-- @return #number Number of units in flight including section members. +-- @return #number Number of units in flight excluding section members. +-- @return #number Number of section members. +function AIRBOSS:_GetFlightUnits(flight, onground) + + -- Default is only airborne. + local inair=true + if onground==true then + inair=false + end ---- Get number of groups and units in queue. + --- Count units of a group which are alive and in the air. + local function countunits(_group, inair) + local group=_group --Wrapper.Group#GROUP + local units=group:GetUnits() + local n=0 + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + if inair then + -- Only count units in air. + if unit:InAir() then + n=n+1 + end + else + -- Count units in air or on the ground. + n=n+1 + end + end + end + return n + end + + + -- Count units of the group itself (alive units in air). + local nunits=countunits(flight.group, inair) + + -- Count section members. + local nsection=0 + for _,sec in pairs(flight.section) do + local secflight=sec --#AIRBOSS.PlayerData + -- Count alive units in air. + nsection=nsection+countunits(secflight.group, inair) + end + + return nunits+nsection, nunits, nsection +end + +--- Get number of groups and units in queue, which are alive and airborne. In units we count the section members as well. -- @param #AIRBOSS self -- @param #table queue The queue. Can be self.flights, self.Qmarshal or self.Qpattern. -- @param #number case (Optional) Only count flights, which are in a specific recovery case. Note that you can use case=23 for flights that are either in Case II or III. By default all groups/units regardless of case are counted. @@ -4089,21 +4845,7 @@ end function AIRBOSS:_GetQueueInfo(queue, case) local ngroup=0 - local nunits=0 - - --- Count units of a group which are alive and in the air. - local function countunitsinair(_group) - local group=_group --Wrapper.Group#GROUP - local units=group:GetUnits() - local n=0 - for _,_unit in pairs(units) do - local unit=_unit --Wrapper.Unit#UNIT - if unit:IsAlive() and unit:InAir() then - n=n+1 - end - end - return n - end + local Nunits=0 -- Loop over flight groups. for _,_flight in pairs(queue) do @@ -4111,42 +4853,48 @@ function AIRBOSS:_GetQueueInfo(queue, case) -- Check if a specific case was requested. if case then - + + ------------------------------------------------------------------------ -- Only count specific case with special 23 = CASE II and III combined. + ------------------------------------------------------------------------ + if (flight.case==case) or (case==23 and (flight.case==2 or flight.case==3)) then - - --ngroup=ngroup+1 - --nunits=nunits+flight.nunits - - -- Count alive units in air. - local n=countunitsinair(flight.group) - if n>0 then + + -- Number of total units, units in flight and section members ALIVE and AIRBORNE. + local ntot,nunits,nsection=self:_GetFlightUnits(flight) + + -- Add up total unit number. + Nunits=Nunits+ntot + + -- Increase group count. + if ntot>0 then ngroup=ngroup+1 - nunits=nunits+n end end else + --------------------------------------------------------------------------- -- No specific case requested. Count all groups & units in selected queue. - --ngroup=ngroup+1 - --nunits=nunits+flight.nunits - - -- Count alive units in air. - local n=countunitsinair(flight.group) - if n>0 then - ngroup=ngroup+1 - nunits=nunits+n - end - - --TODO: add section members? + --------------------------------------------------------------------------- + -- Number of total units, units in flight and section members ALIVE and AIRBORNE. + local ntot,nunits,nsection=self:_GetFlightUnits(flight) + + -- Add up total unit number. + Nunits=Nunits+ntot + + -- Increase group count. + if ntot>0 then + ngroup=ngroup+1 + end + end end - return ngroup, nunits + return ngroup, Nunits end --- Print holding queue. @@ -4171,12 +4919,15 @@ function AIRBOSS:_PrintQueue(queue, name) local fuel=flight.group:GetFuelMin()*100 local ai=tostring(flight.ai) local lead=flight.seclead - local nsec=#flight.section + local Nsec=#flight.section local actype=self:_GetACNickname(flight.actype) local onboard=flight.onboard local holding=tostring(flight.holding) - -- TODO: Include player data. + -- Airborne units. + local _, nunits, nsec=self:_GetFlightUnits(flight, false) + + -- XXX: Include player data. --[[ if not flight.ai then local playerData=_flight --#AIRBOSS.PlayerData @@ -4193,8 +4944,8 @@ function AIRBOSS:_PrintQueue(queue, name) k=playerData.waveoff end ]] - text=text..string.format("\n[%d] %s*%d (%s): lead=%s (%d), onboard=%s, flag=%d, case=%d, time=%s, fuel=%d, ai=%s, holding=%s", - i, flight.groupname, flight.nunits, actype, lead, nsec, onboard, stack, case, clock, fuel, ai, holding) + text=text..string.format("\n[%d] %s*%d (%s): lead=%s (%d/%d), onboard=%s, flag=%d, case=%d, time=%s, fuel=%d, ai=%s, holding=%s", + i, flight.groupname, nunits, actype, lead, nsec, Nsec, onboard, stack, case, clock, fuel, ai, holding) if stack>0 then local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, case)) text=text..string.format(" stackalt=%d ft", alt) @@ -4243,11 +4994,14 @@ function AIRBOSS:_CreateFlightGroup(group) flight.ballcall=false flight.holding=nil + -- TODO Name should also be set for AI as it is used to get the section lead. Switch this from PlayerData to FlightGroup enumerator. + flight.name=flight.group:GetUnit(1):GetName() + -- Note, this should be re-set elsewhere! flight.case=self.case -- Flight elements. - local text=string.format("Flight elemets of group %s:", flight.groupname) + local text=string.format("Flight elements of group %s:", flight.groupname) flight.elements={} local units=group:GetUnits() for i,_unit in pairs(units) do @@ -4270,7 +5024,7 @@ function AIRBOSS:_CreateFlightGroup(group) else flight.onboard=self:_GetOnboardNumberPlayer(group) end - + -- Add to known flights. table.insert(self.flights, flight) @@ -4321,6 +5075,11 @@ function AIRBOSS:_NewPlayer(unitname) -- Set difficulty level. playerData.difficulty=playerData.difficulty or self.defaultskill + -- Subtitles of player + if playerData.subtitles==nil then + playerData.subtitles=true + end + -- Points rewarded. playerData.points={} @@ -4334,7 +5093,7 @@ function AIRBOSS:_NewPlayer(unitname) self.playerscores[playername]=self.playerscores[playername] or {} -- Welcome player message. - self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), "AIRBOSS", "", 5) + self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), string.format("AIRBOSS %s", self.alias), "", 5) -- Return player data table. return playerData @@ -4371,7 +5130,7 @@ function AIRBOSS:_InitPlayer(playerData, step) -- Set us up on final if group name contains "Groove". But only for the first pass. if playerData.group:GetName():match("Groove") and playerData.passes==0 then self:MessageToPlayer(playerData, "Group name contains \"Groove\". Happy groove testing.") - playerData.attitudemonitor=true + playerData.attitudemonitor=false playerData.step=AIRBOSS.PatternStep.FINAL end @@ -4494,7 +5253,7 @@ function AIRBOSS:_RemoveDeadFlightGroups() end ---- Remove a flight group from the Marshal queue. Marshal stack is collapsed, too, if flight was in the queue. +--- Remove a flight group from the Marshal queue. Marshal stack is collapsed, too, if flight was in the queue. Waiting flights are send to marshal. -- @param #AIRBOSS self -- @param #AIRBOSS.FlightGroup flight Flight group that will be removed from queue. -- @param #boolean nopattern If true, flight is NOT going to landing pattern. @@ -4506,7 +5265,40 @@ function AIRBOSS:_RemoveFlightFromMarshalQueue(flight, nopattern) -- Collapse marshal stack if flight was removed. if removed then + + -- Flight is not holding any more. + flight.holding=nil + + -- Collapse marshal stack if flight was removed. self:_CollapseMarshalStack(flight, nopattern) + + -- Stacks are only limited for Case I. + if flight.case==1 and #self.Qwaiting>0 then + + -- Next flight in line waiting. + local nextflight=self.Qwaiting[1] --#AIRBOSS.FlightGroup + + -- Get free stack. + local freestack=self:_GetFreeStack(nextflight.ai) + + -- Send next flight to marshal stack. + if nextflight.ai then + + -- Send AI to Marshal Stack. + self:_MarshalAI(nextflight, freestack) + + else + + -- Send player to Marshal stack. + self:_MarshalPlayer(nextflight, freestack) + + end + + -- Remove flight from waiting queue. + self:_RemoveFlightFromQueue(self.Qwaiting, nextflight) + + end + end return removed @@ -4559,16 +5351,11 @@ function AIRBOSS:_RemoveUnitFromFlight(unit) if removed then - -- Decrease number of units in group. - flight.nunits=flight.nunits-1 + -- Get number of units (excluding section members). For AI only those that are stil in air as we assume once they landed, they are out of the game. + local _,nunits=self:_GetFlightUnits(flight, not flight.ai) - -- Check if numbers still match. - if #flight.elements~=flight.nunits then - self:E("ERROR: Number of elements != number of units in flight!") - end - -- Check if no units are left. - if flight.nunits==0 then + if nunits==0 then -- Remove flight from all queues. self:_RemoveFlight(flight) end @@ -4580,7 +5367,7 @@ function AIRBOSS:_RemoveUnitFromFlight(unit) end ---- Remove a flight from Marshal and Pattern queues. If flight is in Marhal queue, the above stack is collapsed. +--- Remove a flight from Marshal, Pattern and Waiting queues. If flight is in Marhal queue, the above stack is collapsed. -- Also set player step to undefined if applicable or remove human flight if option *completely* is true. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData flight The flight to be removed. @@ -4588,9 +5375,9 @@ end function AIRBOSS:_RemoveFlight(flight, completely) -- Remove flight from all queues. - --self:_RemoveFlightFromQueue(self.Qmarshal, flight) self:_RemoveFlightFromMarshalQueue(flight, true) self:_RemoveFlightFromQueue(self.Qpattern, flight) + self:_RemoveFlightFromQueue(self.Qwaiting, flight) -- Check if player or AI if flight.ai then @@ -4699,8 +5486,8 @@ function AIRBOSS:_CheckPatternUpdate() if Hchange then -- 99, new final bearing XXX local FB=self:GetFinalBearing(true) - local text=string.format("new final bearing %d.", FB) - self:MessageToAll(text, "MARSHAL", "99", 10) + local text=string.format("new final bearing %03d°.", FB) + self:MessageToMarshal(text, "AIRBOSS", "99", 10) end -- Reset parameters for next update check. @@ -4730,15 +5517,15 @@ function AIRBOSS:_CheckPlayerStatus() -- Check if unit is alive and in air. if unit:IsAlive() then - - -- Display aircraft attitude and other parameters as message text. - if playerData.attitudemonitor then - self:_AttitudeMonitor(playerData) - end -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). if unit:IsInZone(self.zoneCCA) then + -- Display aircraft attitude and other parameters as message text. + if playerData.attitudemonitor then + self:_AttitudeMonitor(playerData) + end + -- Check if player is too close to another aircraft in the pattern. -- TODO: At which steps is the really necessary. Case II/III? if playerData.step==AIRBOSS.PatternStep.INITIAL or @@ -4748,15 +5535,18 @@ function AIRBOSS:_CheckPlayerStatus() playerData.step==AIRBOSS.PatternStep.ABEAM or playerData.step==AIRBOSS.PatternStep.GROOVE_XX or playerData.step==AIRBOSS.PatternStep.GROOVE_IM then - --self:_CheckPlayerPatternDistance(playerData) + --self:_CheckPlayerPatternDistance(playerData) end - -- Foul deck check. + -- Foul deck check. + -- TODO: Put steps check into the subroutine. if playerData.case<3 then -- Case I/II: Check is done, when AC is at the wake according to NATOPS. At the wake we switch to final. if playerData.step==AIRBOSS.PatternStep.FINAL or - playerData.step==AIRBOSS.PatternStep.GROOVE_XX then + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC then self:_CheckFoulDeck(playerData) end @@ -4764,7 +5554,9 @@ function AIRBOSS:_CheckPlayerStatus() else -- Case III: Check is done at 3/4 NM according to NATOPS - if playerData.step==AIRBOSS.PatternStep.GROOVE_XX then + if playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC then self:_CheckFoulDeck(playerData) end @@ -4790,10 +5582,15 @@ function AIRBOSS:_CheckPlayerStatus() -- CASE I/II/III: In holding pattern. self:_Holding(playerData) + elseif playerData.step==AIRBOSS.PatternStep.WAITING then + + -- CASE I: Waiting outside 10 NM zone for next free Marshal stack. + self:_Waiting(playerData) + elseif playerData.step==AIRBOSS.PatternStep.COMMENCING then -- CASE I/II/III: New approach. - self:_Commencing(playerData) + self:_Commencing(playerData, true) elseif playerData.step==AIRBOSS.PatternStep.BOLTER then @@ -4879,20 +5676,24 @@ function AIRBOSS:_CheckPlayerStatus() elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then - -- Debriefing in 6 seconds. - SCHEDULER:New(nil, self._Debrief, {self, playerData}, 6) + -- Debriefing in 5 seconds. + SCHEDULER:New(nil, self._Debrief, {self, playerData}, 5) -- Undefined status. playerData.step=AIRBOSS.PatternStep.UNDEFINED else + -- Error, unknown step! self:E(self.lid..string.format("ERROR: Unknown player step %s. Please report!", tostring(playerData.step))) end + -- Check if player missed a step during Case II/III and allow him to enter the landing pattern. + self:_CheckMissedStepOnEntry(playerData) + else - self:T(self.lid.."WARNING: Player left the CCA!") + self:T2(self.lid.."WARNING: Player unit not inside the CCA!") end else @@ -4904,6 +5705,66 @@ function AIRBOSS:_CheckPlayerStatus() end + +--- Checks if a player is in the pattern queue and has missed a step in Case II/III approach. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_CheckMissedStepOnEntry(playerData) + + -- Conditions to be met: Case II/III, in pattern queue, flag!=42 (will be set to 42 at the end if player missed a step). + local rightcase=playerData.case>1 + local rightqueue=self:_InQueue(self.Qpattern, playerData.group) + local rightflag=playerData.flag:Get()~=-42 + + -- Steps that the player could of missed during Case II/III. + local step=playerData.step + local missedstep=step==AIRBOSS.PatternStep.PLATFORM or step==AIRBOSS.PatternStep.ARCIN or step==AIRBOSS.PatternStep.ARCOUT or step==AIRBOSS.PatternStep.DIRTYUP + + -- Check if player is about to enter the initial or bullseye zones and maybe has missed a step in the pattern. + if rightcase and rightqueue and rightflag then + + -- Get right zone. + local zone=nil + if playerData.case==2 and missedstep then + + zone=self:_GetZoneInitial(playerData.case) + + elseif playerData.case==3 and missedstep then + + zone=self:_GetZoneBullseye(playerData.case) + + end + + -- Zone only exists if player is not at the initial or bullseye step. + if zone then + + -- Check if player is in initial or bullseye zone. + local inzone=playerData.unit:IsInZone(zone) + + -- Relative heading to carrier direction. + local relheading=self:_GetRelativeHeading(playerData.unit, false) + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 then + + -- Player is in one of the initial zones short before the landing pattern. + local text=string.format("you missed an important step in the pattern!\nYour next step would have been %s.", playerData.step) + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 5) + + if playerData.case==2 then + -- Set next step to initial. + playerData.step=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- Set next step to bullseye. + playerData.step=AIRBOSS.PatternStep.BULLSEYE + end + + -- Set flag value to -42. This is the value to ensure that this routine is not called again! + playerData.flag:Set(-42) + end + end + end +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- EVENT functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4917,9 +5778,9 @@ function AIRBOSS:OnEventBirth(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)) + self:T2(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T2(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T2(self.lid.."BIRTH: player = "..tostring(_playername)) if _unit and _playername then @@ -4928,7 +5789,7 @@ function AIRBOSS:OnEventBirth(EventData) local _callsign=_unit:GetCallsign() -- Debug output. - local text=string.format("AIRBOSS: Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName()) + local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName()) self:T(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) @@ -4940,6 +5801,21 @@ function AIRBOSS:OnEventBirth(EventData) self:T2(self.lid..text) return end + + -- Check that coalition of the carrier and aircraft match. + if self:GetCoalition()~=_unit:GetCoalition() then + local text=string.format("Player entered aircraft of other coalition.") + MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:T(self.lid..text) + --[[ + local gid=_group:GetID() + if self.menuadded[gid] then + if AIRBOSS.MenuRoot[gid] then + end + end + ]] + return + end -- Add Menu commands. self:_AddF10Commands(_unitName) @@ -5016,7 +5892,7 @@ function AIRBOSS:OnEventLand(EventData) -- Check if this was a valid approach. if not playerData.valid then -- Player missed at least one step in the pattern. - local text=string.format("You missed at least one important step in the pattern!\nYour next step would have been %s.\nThis pass is INVALID.", playerData.step) + local text=string.format("you missed at least one important step in the pattern!\nYour next step would have been %s.\nThis pass is INVALID.", playerData.step) self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 30, true, 5) -- Reinitialize player data. @@ -5262,6 +6138,35 @@ end -- PATTERN functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Waiting outside 10 NM zone for free Marshal stack. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Waiting(playerData) + + -- Create 10 NM zone around the carrier. + local radius=UTILS.NMToMeters(10) + local zone=ZONE_RADIUS:New("Carrier 10 NM Zone", self.carrier:GetVec2(), radius) + + -- Check if player is inside 10 NM radius of the carrier. + local inzone=playerData.unit:IsInZone(zone) + + -- Time player is waiting. + local Twaiting=timer.getAbsTime()-playerData.time + + -- Warning if player is inside the zone. + if inzone and Twaiting>3*60 and not playerData.warning then + local text=string.format("You are supposed to wait outside the 10 NM zone.") + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 10) + playerData.warning=true + end + + -- Reset warning. + if inzone==false and playerData.warning==true then + playerData.warning=nil + end + +end + --- Holding. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. @@ -5273,7 +6178,11 @@ function AIRBOSS:_Holding(playerData) -- Current stack. local stack=playerData.flag:Get() - -- Pattern alitude. + --------------------------- + -- Holding Pattern Check -- + --------------------------- + + -- Pattern altitude. local patternalt=self:_GetMarshalAltitude(stack, playerData.case) -- Player altitude. @@ -5285,7 +6194,7 @@ function AIRBOSS:_Holding(playerData) -- Check if player is in holding zone. local inholdingzone=unit:IsInZone(zoneHolding) - -- Altitude difference between player and assinged stack. + -- Altitude difference between player and assigned stack. local altdiff=playeralt-patternalt -- Acceptable altitude depending on player skill. @@ -5299,11 +6208,12 @@ function AIRBOSS:_Holding(playerData) elseif playerData.difficulty==AIRBOSS.Difficulty.EASY then -- Students should be within +-500 ft. altgood=UTILS.FeetToMeters(500) - else - -- ERROR end - -- Check if stack just collapsed and give the player one minute to change the alitude. + -- When back to good altitude = 50%. + local altback=altgood*0.5 + + -- Check if stack just collapsed and give the player one minute to change the altitude. local justcollapsed=false if self.Tcollapse then -- Time since last stack change. @@ -5367,7 +6277,7 @@ function AIRBOSS:_Holding(playerData) end -- Back to assigned altitude. - if playerData.warning and math.abs(altdiff)<=altgood then + if playerData.warning and math.abs(altdiff)<=altback then text=text..string.format("Altitude is looking good again.") playerData.warning=nil end @@ -5425,45 +6335,87 @@ function AIRBOSS:_Holding(playerData) end -- Send message. - self:MessageToPlayer(playerData, text, "MARSHAL", nil, 5) + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 5) end ---- Commence approach. This step initializes the player data. Next step depends on recovery case: +--- Commence approach. This step initializes the player data. Section members are also set to commence. Next step depends on recovery case: -- -- * Case 1: Initial -- * Case 2/3: Platform -- -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -function AIRBOSS:_Commencing(playerData) +-- @param #boolean zonecheck If true, zone is checked before player is released. +function AIRBOSS:_Commencing(playerData, zonecheck) + + -- Check for auto commence + if zonecheck then + + -- Get auto commence zone. + local zoneCommence=self:_GetZoneCommence(playerData.case) + + -- Check if unit is in the zone. + local inzone=playerData.unit:IsInZone(zoneCommence) + + -- Skip the rest if not in the zone yet. + if not inzone then + return + end + + end + + -- Remove flight from Marshal queue. If flight was in queue, stack is collapsed and flight added to the pattern queue. + self:_RemoveFlightFromMarshalQueue(playerData) -- Initialize player data for new approach. self:_InitPlayer(playerData) - + -- Commencing message to player only. if playerData.difficulty==AIRBOSS.Difficulty.EASY then - local text=string.format("Commencing. (Case %d)", playerData.case) - self:MessageToPlayer(playerData, text, playerData.onboard, "", 5) + + -- Text + local text="" + + -- Positive response. + if playerData.case==1 then + text=text.."Proceed to initial." + else + text=text.."Descent to platform." + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + text=text.." VSI 4000 ft/min until you reach 5000 ft." + end + end + + -- Message to player. + self:MessageToPlayer(playerData, text, "MARSHAL", nil, 3) end -- Next step: depends on case recovery. + local nextstep if playerData.case==1 then -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. - playerData.step=AIRBOSS.PatternStep.INITIAL + nextstep=AIRBOSS.PatternStep.INITIAL else -- CASE II/III: Player has to start the descent at 4000 ft/min to the platform at 5k ft. - playerData.step=AIRBOSS.PatternStep.PLATFORM + nextstep=AIRBOSS.PatternStep.PLATFORM + end + + -- Next step hint. + self:_SetPlayerStep(playerData, nextstep) + + -- Commence section members as well but dont check the zone. + for i,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + self:_Commencing(flight, false) end - -- Next step hint. - self:_StepHint(playerData) - playerData.warning=nil end --- Start pattern when player enters the initial zone in case I/II recoveries. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #boolean True if player is in the inital zone. function AIRBOSS:_Initial(playerData) -- Check if player is in initial zone and entering the CASE I pattern. @@ -5490,11 +6442,12 @@ function AIRBOSS:_Initial(playerData) end -- Next step: Break entry. - playerData.step=AIRBOSS.PatternStep.BREAKENTRY - playerData.warning=nil - self:_StepHint(playerData) + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.BREAKENTRY) + + return true end + return false end --- Check if player is in CASE II/III approach corridor. @@ -5510,13 +6463,13 @@ function AIRBOSS:_CheckCorridor(playerData) -- Issue warning. if invalid and (not playerData.warning) then - self:MessageToPlayer(playerData, "You left the approach corridor!", "MARSHAL") + self:MessageToPlayer(playerData, "You left the approach corridor!", "AIRBOSS") playerData.warning=true end -- Back in zone. if (not invalid) and playerData.warning then - self:MessageToPlayer(playerData, "You're back in the approach corridor.", "MARSHAL") + self:MessageToPlayer(playerData, "You're back in the approach corridor.", "AIRBOSS") playerData.warning=false end @@ -5558,22 +6511,23 @@ function AIRBOSS:_Platform(playerData) end -- Next step: depends. + local nextstep if math.abs(self.holdingoffset)>0 and playerData.case>1 then -- Turn to BRC (case II) or FB (case III). - playerData.step=AIRBOSS.PatternStep.ARCIN + nextstep=AIRBOSS.PatternStep.ARCIN else if playerData.case==2 then -- Case II: Initial zone then Case I recovery. - playerData.step=AIRBOSS.PatternStep.INITIAL + nextstep=AIRBOSS.PatternStep.INITIAL elseif playerData.case==3 then -- CASE III: Dirty up. - playerData.step=AIRBOSS.PatternStep.DIRTYUP + nextstep=AIRBOSS.PatternStep.DIRTYUP end end -- Next step hint. - self:_StepHint(playerData) - playerData.warning=nil + self:_SetPlayerStep(playerData, nextstep) + end end @@ -5622,9 +6576,8 @@ function AIRBOSS:_ArcInTurn(playerData) end -- Next step: Arc Out Turn. - playerData.step=AIRBOSS.PatternStep.ARCOUT - playerData.warning=nil - self:_StepHint(playerData) + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.ARCOUT) + end end @@ -5658,19 +6611,17 @@ function AIRBOSS:_ArcOutTurn(playerData) end -- Next step: - if playerData.case==2 then - -- Case II: Initial. - playerData.step=AIRBOSS.PatternStep.INITIAL - elseif playerData.case==3 then + local nextstep + if playerData.case==3 then -- Case III: Dirty up. - playerData.step=AIRBOSS.PatternStep.DIRTYUP + nextstep=AIRBOSS.PatternStep.DIRTYUP else - -- ERROR! + -- Case II: Initial. + nextstep=AIRBOSS.PatternStep.INITIAL end -- Next step hint. - self:_StepHint(playerData) - playerData.warning=nil + self:_SetPlayerStep(playerData, nextstep) end end @@ -5722,15 +6673,15 @@ function AIRBOSS:_DirtyUp(playerData) -- TODO: Make Fly Bullseye call if no automatic ICLS is active. -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). - playerData.step=AIRBOSS.PatternStep.BULLSEYE - playerData.warning=nil - self:_StepHint(playerData) + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.BULLSEYE) + end end --- Intercept glide slop and follow ICLS, aka Bullseye for case III recovery. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #boolean If true, player is in bullseye zone. function AIRBOSS:_Bullseye(playerData) -- Check if player left or got back to the approach corridor. @@ -5765,18 +6716,15 @@ function AIRBOSS:_Bullseye(playerData) -- Hint follow the needles. if playerData.difficulty==AIRBOSS.Difficulty.EASY then - hint=hint..string.format("Intercept glide slope and follow the needles.") + hint=hint..string.format("Intercept glideslope and follow the needles.") end self:MessageToPlayer(playerData, hint, "MARSHAL", "") end -- Next step: Groove Call the ball. - playerData.step=AIRBOSS.PatternStep.GROOVE_XX - playerData.warning=nil + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) - -- Stephint should be empty. - self:_StepHint(playerData) end end @@ -5802,13 +6750,14 @@ function AIRBOSS:_BolterPattern(playerData) -- Check if we are in front of the boat (diffX > 0). if self:_CheckLimits(X, Z, Bolter) then + local nextstep if playerData.case<3 then - playerData.step=AIRBOSS.PatternStep.ABEAM + nextstep=AIRBOSS.PatternStep.ABEAM else - playerData.step=AIRBOSS.PatternStep.BULLSEYE + nextstep=AIRBOSS.PatternStep.BULLSEYE end - playerData.warning=nil - self:_StepHint(playerData) + self:_SetPlayerStep(playerData, nextstep) + end end @@ -5845,9 +6794,8 @@ function AIRBOSS:_BreakEntry(playerData) end -- Next step: Early Break. - playerData.step=AIRBOSS.PatternStep.EARLYBREAK - playerData.warning=nil - self:_StepHint(playerData) + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.EARLYBREAK) + end end @@ -5900,14 +6848,14 @@ function AIRBOSS:_Break(playerData, part) self:_AddToDebrief(playerData, debrief) -- Next step: Late Break or Abeam. + local nextstep if part==AIRBOSS.PatternStep.EARLYBREAK then - playerData.step=AIRBOSS.PatternStep.LATEBREAK + nextstep=AIRBOSS.PatternStep.LATEBREAK else - playerData.step=AIRBOSS.PatternStep.ABEAM + nextstep=AIRBOSS.PatternStep.ABEAM end - playerData.warning=nil - self:_StepHint(playerData) + self:_SetPlayerStep(playerData, nextstep) end end @@ -5937,8 +6885,8 @@ function AIRBOSS:_CheckForLongDownwind(playerData) playerData.patternwo=true -- Next step: Debriefing. - playerData.step=AIRBOSS.PatternStep.DEBRIEF - playerData.warning=nil + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) + end end @@ -5988,9 +6936,8 @@ function AIRBOSS:_Abeam(playerData) self:_AddToDebrief(playerData, debrief) -- Next step: ninety. - playerData.step=AIRBOSS.PatternStep.NINETY - playerData.warning=nil - self:_StepHint(playerData) + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.NINETY) + end end @@ -6036,9 +6983,7 @@ function AIRBOSS:_Ninety(playerData) self:_AddToDebrief(playerData, debrief) -- Next step: wake. - playerData.step=AIRBOSS.PatternStep.WAKE - playerData.warning=nil - self:_StepHint(playerData) + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.WAKE) elseif relheading>90 and self:_CheckLimits(X, Z, self.Wake) then -- Message to player. @@ -6086,9 +7031,8 @@ function AIRBOSS:_Wake(playerData) self:_AddToDebrief(playerData, debrief) -- Next step: Final. - playerData.step=AIRBOSS.PatternStep.FINAL - playerData.warning=nil - self:_StepHint(playerData) + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.FINAL) + end end @@ -6154,10 +7098,9 @@ function AIRBOSS:_Final(playerData) -- Groove data. playerData.groove.X0=groovedata - -- Next step: X start & call the ball. - playerData.step=AIRBOSS.PatternStep.GROOVE_XX - playerData.warning=nil - self:_StepHint(playerData) + -- Next step: X start. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) + end end @@ -6191,7 +7134,7 @@ function AIRBOSS:_Groove(playerData) -- Lineup with runway centerline. local lineupError=self:_Lineup(playerData.unit, true) - -- Glide slope. + -- Glideslope. local glideslopeError=self:_Glideslope(playerData.unit, 3.5) -- Get AoA. @@ -6341,7 +7284,7 @@ function AIRBOSS:_Groove(playerData) gd.LUE=lineupError end - -- Fly through good window of glide slope. + -- Fly through good window of glideslope. if gd.GSE>0.4 and glideslopeError<-0.3 then -- Fly through down ==> "\" gd.FlyThrough="\\" @@ -6352,9 +7295,9 @@ function AIRBOSS:_Groove(playerData) self:T(self.lid..string.format("Got Fly through UP at %s. Min GSE=%.1f, lower GSE=%.1f", gs, gd.GSE, glideslopeError)) end - -- Update max deviation of glide slope error. + -- Update max deviation of glideslope error. if math.abs(glideslopeError)>math.abs(gd.GSE) then - self:T(self.lid..string.format("Got bigger glide slope error at %s: GSE |%.3f|>|%.3f|.", gs, glideslopeError, gd.GSE)) + self:T(self.lid..string.format("Got bigger glideslope error at %s: GSE |%.3f|>|%.3f|.", gs, glideslopeError, gd.GSE)) gd.GSE=glideslopeError end @@ -6383,7 +7326,7 @@ function AIRBOSS:_Groove(playerData) local deltaT=timer.getTime()-playerData.Tlso -- LSO call if necessary. - if deltaT>=3 then + if deltaT>=self.LSOdT then self:_LSOadvice(playerData, glideslopeError, lineupError) end @@ -6433,11 +7376,11 @@ end --- LSO check if player needs to wave off. -- Wave off conditions are: -- --- * Glide slope error <1.2 or >1.8 degrees. +-- * Glideslope error <1.2 or >1.8 degrees. -- * |Line up error| > 3 degrees. -- * AoA check but only for TOPGUN graduates. -- @param #AIRBOSS self --- @param #number glideslopeError Glide slope error in degrees. +-- @param #number glideslopeError Glideslope error in degrees. -- @param #number lineupError Line up error in degrees. -- @param #number AoA Angle of attack of player aircraft. -- @param #AIRBOSS.PlayerData playerData Player data. @@ -6449,12 +7392,12 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) -- Too high or too low? if glideslopeError>1.8 then - local text=string.format("Wave off due to glide slope error %.2f > 1.8 degrees!", glideslopeError) + local text=string.format("Wave off due to glideslope error %.2f > 1.8 degrees!", glideslopeError) self:T(self.lid..string.format("%s: %s", playerData.name, text)) self:_AddToDebrief(playerData, text) waveoff=true elseif glideslopeError<-1.2 then - local text=string.format("Wave off due to glide slope error %.2f < -1.2 degrees!", glideslopeError) + local text=string.format("Wave off due to glideslope error %.2f < -1.2 degrees!", glideslopeError) self:T(self.lid..string.format("%s: %s", playerData.name, text)) self:_AddToDebrief(playerData, text) waveoff=true @@ -6495,6 +7438,12 @@ end -- @return boolean If true, we have a foul deck. function AIRBOSS:_CheckFoulDeck(playerData) + -- Check if player was already waved off. Should not be necessary as player step is set to debrief afterwards! + if playerData.fouldeckwo==true then + -- Player was already waved off. + return + end + -- Landing runway zone. local runway=self:_GetZoneRunwayBox() @@ -6541,7 +7490,7 @@ function AIRBOSS:_CheckFoulDeck(playerData) -- Foul deck + wave off radio message. self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.FOULDECK, false, 1) - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WAVEOFF, false, 1) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WAVEOFF, false, 1.2) -- Player hint for flight students. if playerData.difficulty==AIRBOSS.Difficulty.EASY then @@ -6559,7 +7508,7 @@ function AIRBOSS:_CheckFoulDeck(playerData) if foulunit then local foulflight=self:_GetFlightFromGroupInQueue(foulunit:GetGroup(), self.flights) if foulflight and not foulflight.ai then - self:MessageToPlayer(foulflight, "Move your ass away from my runway!", "MARSHAL", nil, 10) + self:MessageToPlayer(foulflight, "Move your ass from my runway. NOW!", "AIRBOSS", nil, 10) end end end @@ -7137,15 +8086,12 @@ function AIRBOSS:_GetZoneHolding(case, stack) -- Distance to the post. local D=UTILS.NMToMeters(2.5) - - -- Radius of the zone. Diameter 5 NM +10% error margin. - local R=D*1.1 - + -- Post 2.5 NM port of carrier. local Post=self:GetCoordinate():Translate(D, hdg+270) -- Create holding zone. - zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", Post:GetVec2(), R) + zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", Post:GetVec2(), self.marshalradius) else -- CASE II/II @@ -7153,12 +8099,12 @@ function AIRBOSS:_GetZoneHolding(case, stack) -- Get radial. local radial=self:GetRadial(case, false, true) - -- Create an array of a square! + -- Create an array of a rectangle. Length is 7 NM, width is 8 NM. One NM starboard to line up with the approach corridor. local p={} p[1]=c2:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c2 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. p[2]=c1:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c1 is 7 NM further behind. Also translated 1 NM starboard. - p[3]=c1:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p3 6 NM port of carrier. - p[4]=c2:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p4 6 NM port of carrier. + p[3]=c1:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p3 7 NM port of carrier. + p[4]=c2:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p4 7 NM port of carrier. -- Square zone length=7NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. -- So stay 0-5 NM (+1 NM error margin) port of carrier. @@ -7168,6 +8114,44 @@ function AIRBOSS:_GetZoneHolding(case, stack) return zoneHolding end +--- Get zone where player are automatically commence when enter. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE Holding zone. +function AIRBOSS:_GetZoneCommence(case) + + -- Commence zone. + local zone + + if case==1 then + -- Case I + + -- Get current carrier heading. + local hdg=self:GetHeading() + + -- Distance to the zone. + local D=UTILS.NMToMeters(4.75) + + -- Zone radius. + local R=UTILS.NMToMeters(1) + + -- Three position + local Three=self:GetCoordinate():Translate(D, hdg+275) + + -- Create holding zone. + zone=ZONE_RADIUS:New("CASE I Holding Zone", Three:GetVec2(), R) + + else + -- Case II/III + + -- We simply take the corridor for now. + zone=self:_GetZoneCorridor(case) + + end + + return zone +end + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ORIENTATION functions ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -7201,7 +8185,7 @@ function AIRBOSS:_AttitudeMonitor(playerData) local relhead=self:_GetRelativeHeading(playerData.unit) local step=playerData.step - if playerData.step==AIRBOSS.PatternStep.GROOVE_X0 or + if playerData.step==AIRBOSS.PatternStep.FINAL or playerData.step==AIRBOSS.PatternStep.GROOVE_XX or playerData.step==AIRBOSS.PatternStep.GROOVE_IM or playerData.step==AIRBOSS.PatternStep.GROOVE_IC or @@ -7222,7 +8206,7 @@ function AIRBOSS:_AttitudeMonitor(playerData) text=text..string.format("\nPitch=%.1f° | Roll=%.1f° | Yaw=%.1f°", pitch, roll, yaw) text=text..string.format("\nClimb Angle=%.1f° | Rate=%d ft/min", unit:GetClimbAngle(), velo.y*196.85) -- If in the groove, provide line up and glide slope error. - if playerData.step==AIRBOSS.PatternStep.GROOVE_X0 or + if playerData.step==AIRBOSS.PatternStep.FINAL or playerData.step==AIRBOSS.PatternStep.GROOVE_XX or playerData.step==AIRBOSS.PatternStep.GROOVE_IM or playerData.step==AIRBOSS.PatternStep.GROOVE_IC or @@ -8652,7 +9636,7 @@ function AIRBOSS:_Debrief(playerData) end end ---- Hind for flight students about the (next) step. +--- Display hint for flight students about the (next) step. Message is displayed after one second. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. -- @param #string step Step for which hint is given. @@ -8697,7 +9681,7 @@ function AIRBOSS:_StepHint(playerData, step) local text=string.format("Optimal setup at next step %s:%s", step, hint) -- Send hint to player. - self:MessageToPlayer(playerData, text, "AIRBOSS", "", 10, false, 2) + self:MessageToPlayer(playerData, text, "AIRBOSS", "", 10, false, 1) end @@ -8980,14 +9964,18 @@ function AIRBOSS:GetCoordinate() end ---- Get mission weather. +--- Get static weather of this mission from env.mission.weather. -- @param #AIRBOSS self -function AIRBOSS:_MissionWeather() +-- @param #table Clouds table which has entries "thickness", "density", "base", "iprecptns". +-- @param #number Visibility distance in meters. +-- @param #table Fog table, which has entries "thickness", "visibility" or nil if fog is disabled in the mission. +-- @param #number Dust density or nil if dust is disabled in the mission. +function AIRBOSS:_GetStaticWeather() -- Weather data from mission file. local weather=env.mission.weather - + -- Clouds --[[ ["clouds"] = { @@ -8999,18 +9987,35 @@ function AIRBOSS:_MissionWeather() ]] local clouds=weather.clouds - --[[ + -- Visibilty distance in meters. + local visibility=weather.visibility.distance + + -- Dust + --[[ + ["enable_dust"] = false, + ["dust_density"] = 0, + ]] + local dust=nil + if weather.enable_dust==true then + dust=weather.dust_density + end + + -- Fog + --[[ + ["enable_fog"] = false, ["fog"] = { ["thickness"] = 0, ["visibility"] = 25, }, -- end of ["fog"] - ]] - local fog=weather.fog + ]] + local fog=nil + if weather.enable_fog==true then + fog=weather.fog + end - -- Visibilty distance in meters. - local vis=weather.visibility.distance + return clouds, visibility, fog, dust end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -9194,7 +10199,17 @@ function AIRBOSS:RadioTransmit(radio, call, loud, delay) end -- Message "Subtitle" to all players. - self:MessageToAll(subtitle, radio:GetAlias(), "", call.duration) + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Message to all players in CCA that have subtites on. + if playerData.unit:IsInZone(self.zoneCCA) and playerData.subtitles then + + -- Message to player. + self:MessageToPlayer(playerData, subtitle, radio:GetAlias(), "", call.duration) + + end + end else @@ -9239,21 +10254,21 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear, 0, soundoff}, delay) else - if sender and not soundoff then + if not soundoff then if receiver=="99" then -- Radio message from LSO or MARSHAL to all. - if sender=="LSO" then + if sender and sender=="LSO" then self:_Number2Radio(self.LSORadio, receiver, delay) - elseif sender=="MARSHAL" then + elseif sender and (sender=="MARSHAL" or sender=="AIRBOSS") then self:_Number2Radio(self.MarshalRadio, receiver, delay) end elseif receiver==playerData.onboard then -- Sound only to player group. - if sender=="LSO" or sender=="MARSHAL" or sender=="AIRBOSS" then + if sender and (sender=="LSO" or sender=="MARSHAL" or sender=="AIRBOSS") then self:_Number2Sound(playerData, sender, receiver, delay) end @@ -9270,36 +10285,6 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration end end ---- Send text message to all players in the CCA. --- Message format will be "SENDER: RECCEIVER, MESSAGE". --- @param #AIRBOSS self --- @param #string message The message to send. --- @param #string sender The person who sends the message or nil. --- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. --- @param #number duration Display message duration. Default 10 seconds. --- @param #boolean clear If true, clear screen from previous messages. --- @param #number delay Delay in seconds, before the message is displayed. --- @param #boolean soundoff If true, do not play boad number message. -function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay, soundoff) - - -- Make sure the onboard number sound is played only once. - local soundoff=false - - for _,_player in pairs(self.players) do - local playerData=_player --#AIRBOSS.PlayerData - - -- Message to all players in CCA. - if playerData.unit:IsInZone(self.zoneCCA) then - - -- Message to player. - self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) - - -- Disable sound play of onboard number. - soundoff=true - end - end -end - --- Send text message to all players in the pattern queue. -- Message format will be "SENDER: RECCEIVER, MESSAGE". @@ -9313,21 +10298,35 @@ end -- @param #boolean soundoff If true, do not play boad number message. function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, delay, soundoff) - -- Make sure the onboard number sound is played only once. - local soundoff=false + -- Local delay. + local _delay=delay or 0 + + -- Broadcast onboard number on Marshal frequency if sender. + if self:_IsOnboard(sender) then + self:_Number2Radio(self.LSORadio, sender, _delay) + _delay=_delay+3 + end + + if self:_IsOnboard(receiver) or receiver=="99" then + self:_Number2Radio(self.LSORadio, receiver, _delay) + end -- Loop over all flights in the pattern queue. - for _,_player in pairs(self.Qpattern) do - local playerData=_player --#AIRBOSS.PlayerData + for _,_player in pairs(self.players) do + local player=_player --#AIRBOSS.PlayerData - -- Message only to human pilots. - if not playerData.ai then - -- Message to player. - self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) + -- Only to players inside CCA. + if player and player.unit:IsInZone(self.zoneCCA) then + + -- To all if adressed to 99 or only to flights in the pattern queue. + if receiver=="99" or self:_InQueue(self.Qpattern, player.group) then + + -- Message to player. + self:MessageToPlayer(player, message, sender, receiver, duration, clear, delay, true) + + end - -- Disable sound play of onboard number. - soundoff=true end end end @@ -9341,26 +10340,66 @@ end -- @param #number duration Display message duration. Default 10 seconds. -- @param #boolean clear If true, clear screen from previous messages. -- @param #number delay Delay in seconds, before the message is displayed. --- @param #boolean soundoff If true, do not play boad number message. -function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, delay, soundoff) - - -- Make sure the onboard number sound is played only once. - local soundoff=false +function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, delay) - -- Loop over all flights in the marshal queue. - for _,_player in pairs(self.Qmarshal) do - local playerData=_player --#AIRBOSS.PlayerData - - -- Message only to human pilots. - if not playerData.ai then - - -- Message to player. - self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) - - -- Disable sound play of onboard number. - soundoff=true - end + -- Local delay. + local _delay=delay or 0 + + -- Broadcast onboard number on Marshal frequency if sender is onboard. + if self:_IsOnboard(sender) then + self:_Number2Radio(self.MarshalRadio, sender, _delay) + _delay=_delay+3 end + + -- Broadcast onboard number on Marshal frequency if receiver is onboard. + if self:_IsOnboard(receiver) or receiver=="99" then + self:_Number2Radio(self.MarshalRadio, receiver, _delay) + end + + -- Loop over all flights in the marshal queue. + for _,_player in pairs(self.players) do + local player=_player --#AIRBOSS.PlayerData + + -- Only to players inside CCA. + if player and player.unit:IsInZone(self.zoneCCA) then + + -- To all if adressed to 99 or only to flights in the Marshal or Waiting queue. + --if receiver=="99" or self:_InQueue(self.Qmarshal, player.group) or self:_InQueue(self.Qwaiting, player.group) then + + -- Message to player. + self:MessageToPlayer(player, message, sender, receiver, duration, clear, delay, true) + + --end + + end + end +end + +--- Check if text is an onboard number of a flight. +-- @param #AIRBOSS self +-- @param #string text Text to check. +-- @return #boolean If true, text is an onboard number of a flight. +function AIRBOSS:_IsOnboard(text) + + -- Nil check. + if text==nil then + return false + end + + -- Loop over all flights. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Loop over all onboard number of that flight. + for _,onboard in pairs(flight.onboardnumbers) do + if text==onboard then + return true + end + end + + end + + return false end --- Convert a number (as string) into an outsound and play it to a player group. E.g. for board number or headings. @@ -9522,7 +10561,9 @@ function AIRBOSS:_AddF10Commands(_unitName) end -- F10/Airboss/ - local _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) + local _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) + --AIRBOSS.MenuRoot[gid]=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) + --local _rootPath=AIRBOSS.MenuRoot[gid] -------------------------------- -- F10/Airboss//F1 Help @@ -9552,6 +10593,7 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._DisplayAttitude, self, _unitName) -- F4 missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) -- F5 missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) -- F6 + missionCommands.addCommandForGroup(gid, "Subtitles On/Off", _helpPath, self._SubtitlesOnOff, self, _unitName) -- F7 ------------------------------------- -- F10/Airboss//F2 Kneeboard @@ -9569,6 +10611,7 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(gid, "Set Section", _kneeboardPath, self._SetSection, self, _unitName) -- F4 missionCommands.addCommandForGroup(gid, "Marshal Queue", _kneeboardPath, self._DisplayQueue, self, _unitName, self.Qmarshal, "Marshal") -- F5 missionCommands.addCommandForGroup(gid, "Pattern Queue", _kneeboardPath, self._DisplayQueue, self, _unitName, self.Qpattern, "Pattern") -- F6 + missionCommands.addCommandForGroup(gid, "Waiting Queue", _kneeboardPath, self._DisplayQueue, self, _unitName, self.Qwaiting, "Waiting") -- F7 ------------------------- -- F10/Airboss// @@ -9610,11 +10653,6 @@ function AIRBOSS:_ResetPlayerStatus(_unitName) local text="Status reset executed! You have been removed from all queues." self:MessageToPlayer(playerData, text, "AIRBOSS") - -- Remove from marhal stack can collapse stack if necessary. - --if self:_InQueue(self.Qmarshal, playerData.group) then - -- self:_CollapseMarshalStack(playerData, true) - --end - -- Remove flight from queues. Collapse marshal stack if necessary. self:_RemoveFlight(playerData) @@ -9657,23 +10695,48 @@ function AIRBOSS:_RequestMarshal(_unitName) local text=string.format("you are already in the Pattern queue. Marshal request denied!") self:MessageToPlayer(playerData, text, "MARSHAL") + elseif self:_InQueue(self.Qwaiting, playerData.group) then + + -- Flight group is already in pattern queue. + local text=string.format("you are in the Waiting queue with %d flights ahead of you. Marshal request denied!", #self.Qwaiting) + self:MessageToPlayer(playerData, text, "MARSHAL") + elseif not _unit:InAir() then -- Flight group is already in pattern queue. local text=string.format("you are not airborne. Marshal request denied!") self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif playerData.name~=playerData.seclead then + + -- Flight group is already in pattern queue. + local text=string.format("negative, your section lead %s needs to request Marshal.", playerData.seclead) + self:MessageToPlayer(playerData, text, "MARSHAL") else - - -- Add flight to marshal stack. - self:_MarshalPlayer(playerData) + + -- Get next free Marshal stack. + local freestack=self:_GetFreeStack(playerData.ai) + + -- Check if stack is available. For Case I the number is limited. + if freestack then + + -- Add flight to marshal stack. + self:_MarshalPlayer(playerData, freestack) + + else + + -- Add flight to waiting queue. + self:_WaitPlayer(playerData) + + end end else -- Flight group is not in CCA yet. - local text=string.format("you are not inside CCA yet. Marshal request denied!") + local text=string.format("you are not inside CCA. Marshal request denied!") self:MessageToPlayer(playerData, text, "MARSHAL") end @@ -9681,7 +10744,7 @@ function AIRBOSS:_RequestMarshal(_unitName) end end ---- Request to commence approach. +--- Request to commence landing approach. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. function AIRBOSS:_RequestCommence(_unitName) @@ -9695,90 +10758,105 @@ function AIRBOSS:_RequestCommence(_unitName) local playerData=self.players[_playername] --#AIRBOSS.PlayerData if playerData then - + -- Check if unit is in CCA. local text="" if _unit:IsInZone(self.zoneCCA) then + -- Get stack value. + local stack=playerData.flag:Get() + + -- Number of airborne aircraft currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + -- TODO: Check distance to initial or platform. Only allow commence if < max distance. Otherwise say bearing. if self:_InQueue(self.Qpattern, playerData.group) then -- Flight group is already in pattern queue. - text=string.format("%s, you are already in the Pattern queue. Commence request denied!", playerData.name) + text=string.format("negative, %s, you are already in the Pattern queue.", playerData.name) elseif not _unit:InAir() then -- Flight group is already in pattern queue. - text=string.format("%s, you are not airborne. Commence request denied!", playerData.name) - - else + text=string.format("negative, %s, you are not airborne.", playerData.name) + + elseif playerData.seclead~=playerData.name then + + -- Flight group is already in pattern queue. + text=string.format("negative, %s, your section leader %s has to request commence!", playerData.name, playerData.seclead) - -- Get stack value. - local stack=playerData.flag:Get() + elseif stack>1 then + + -- We are in a higher stack. + text=string.format("negative, %s, it's not your turn yet! You are in stack no. %s.", playerData.name, stack) + + elseif npattern>=self.Nmaxpattern then - -- Check if player is in the lowest stack. - if stack>1 then - -- We are in a higher stack. - text="Negative ghostrider, it's not your turn yet!" + -- Patern is full! + text=string.format("negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern) + + elseif self:IsRecovering()==false and not self.airbossnice then + + -- Carrier is not recovering right now. + if self.recoverywindow then + local clock=UTILS.SecondsToClock(self.recoverywindow.START) + text=string.format("negative, carrier is currently not recovery. Next window will open at %s.", clock) else - - -- Number of aircraft currently in pattern. - local _,npattern=self:_GetQueueInfo(self.Qpattern) - - -- Check if pattern is already full. - if npattern>=self.Nmaxpattern then - - -- Patern is full! - text=string.format("Negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern) - - else - - -- TODO: check if recovery window is open. - if not self:IsRecovering() then - text="Recovery window NOT open yet! However, you are cleared anyway.\n" - end - - -- If player is not in the Marshal queue set player case to current case. - if not self:_InQueue(self.Qmarshal, playerData.group) then - - -- Set case. - playerData.case=self.case - - -- Hint about TACAN bearing. - if self.TACANon and playerData.difficulty~=AIRBOSS.Difficulty.HARD then - -- Get inverse magnetic radial potential offset. - local radial=self:GetRadial(playerData.case, true, true, true) - if playerData.case==1 then - -- For case 1 we want the BRC but above routine return FB. - radial=self:GetBRC() - end - text=text..string.format("Select TACAN %03d°, channel %d%s (%s)\n", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) - end - - end - - -- Positive response. - if playerData.case==1 then - text=text.."Proceed to initial." - else - text=text.."Descent at 4k ft/min to platform at 5000 ft." - end - - -- Set player step. - playerData.step=AIRBOSS.PatternStep.COMMENCING - playerData.warning=nil - - -- Remove flight from Marshal queue and collaps stack if necessary. - self:_RemoveFlightFromMarshalQueue(playerData, false) - end - + text=string.format("negative, carrier is not recovering. No future windows planned.") end + elseif not self:_InQueue(self.Qmarshal, playerData.group) and not self.airbossnice then + + text="negatitive, you have to request Marshal before you can commence." + + else + + ----------------------- + -- Positive Response -- + ----------------------- + + -- Carrier is not recovering but Airboss has a good day. + if not self:IsRecovering() then + text="Carrier is not recovering currently! However, you are cleared anyway as I have a nice day.\n" + end + + -- If player is not in the Marshal queue set player case to current case. + if not self:_InQueue(self.Qmarshal, playerData.group) then + + -- Set current case. + playerData.case=self.case + + -- Hint about TACAN bearing. + if self.TACANon and playerData.difficulty~=AIRBOSS.Difficulty.HARD then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(playerData.case, true, true, true) + if playerData.case==1 then + -- For case 1 we want the BRC but above routine return FB. + radial=self:GetBRC() + end + text=text..string.format("Select TACAN %03d°, channel %d%s (%s)\n", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + end + + -- TODO: Inform section members. + + -- Set case of section members as well. Not sure if necessary any more since it is set as soon as the recovery case is changed. + for _,flight in pairs(playerData.section) do + flight.case=playerData.case + end + + -- Add player to pattern queue. Usually this is done when the stack is collapsed but this player is not in the Marshal queue. + table.insert(self.Qpattern, playerData) + end + + -- Call commence routine. No zone check. + -- NOTE: Commencing will set step for all section members as well. + self:_Commencing(playerData, false) end + else -- This flight is not yet registered! - text="Negative ghostrider, you are not inside the CCA yet!" + text=string.format("negative, %s, you are not inside the CCA!", playerData.name) end -- Debug @@ -9834,13 +10912,16 @@ function AIRBOSS:_RequestRefueling(_unitName) -- Collapse marshal stack if player is in queue. self:_RemoveFlightFromMarshalQueue(playerData, true) - -- TODO: What if only the player and not his section wants to refuel?! - --if self:_InQueue(self.Qmarshal, playerData.group) then - -- self:_CollapseMarshalStack(playerData, true) - --end - + -- Set step to refueling. - playerData.step=AIRBOSS.PatternStep.REFUELING + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.REFUELING) + + -- Inform section and set step. + for _,sec in pairs(playerData.section) do + local sectext="Follow you section leader to the tanker." + self:MessageToPlayer(sec, sectext, "MARSHAL") + self:_SetPlayerStep(sec, AIRBOSS.PatternStep.REFUELING) + end elseif self.tanker:IsReturning() then -- Tanker is RTB. @@ -9848,10 +10929,10 @@ function AIRBOSS:_RequestRefueling(_unitName) end else - text="You are not registered inside the CCA yet. Request denied!" + text="negative, you are not inside the CCA yet." end else - text="No refueling tanker available. Request denied!" + text="negative, no refueling tanker available." end -- Send message. @@ -9860,7 +10941,25 @@ function AIRBOSS:_RequestRefueling(_unitName) end end ---- Set all flights within 200 meters to be part of my section. + +--- Remove a member from the player's section. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player +-- @param #AIRBOSS.PlayerData sectionmember The section member to be removed. +-- @return #boolean If true, flight was a section member and could be removed. False otherwise. +function AIRBOSS:_RemoveSectionMember(playerData, sectionmember) + -- Loop over all flights in player's section + for i,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + if flight.name==sectionmember.name then + table.remove(playerData.section, i) + return true + end + end + return false +end + +--- Set all flights within 100 meters to be part of my section. -- @param #AIRBOSS self -- @param #string _unitName Name of the player unit. function AIRBOSS:_SetSection(_unitName) @@ -9877,56 +10976,136 @@ function AIRBOSS:_SetSection(_unitName) -- Coordinate of flight lead. local mycoord=_unit:GetCoordinate() + -- Max distance up to which section members are allowed. + local dmax=100 + -- Check if player is in Marshal or pattern queue already. - local text - if self:_InQueue(self.Qmarshal,playerData.group) then - text=string.format("You are already in the Marshal queue. Setting section not possible any more!") + local text + if self.NmaxSection==0 then + text=string.format("setting sections is disabled in this mission. You stay alone.") + elseif self:_InQueue(self.Qmarshal,playerData.group) then + text=string.format("you are already in the Marshal queue. Setting section not possible any more!") elseif self:_InQueue(self.Qpattern, playerData.group) then - text=string.format("You are already in the Pattern queue. Setting section not possible any more!") + text=string.format("you are already in the Pattern queue. Setting section not possible any more!") else - -- Init array - playerData.section={} - - -- Loop over all registered flights. - for _,_flight in pairs(self.flights) do - local flight=_flight --#AIRBOSS.FlightGroup + -- Check if player is member of another section already. If so, remove him from his current section. + if playerData.seclead~=playerData.name then + local lead=self.players[playerData.seclead] --#AIRBOSS.PlayerData + if lead then - -- Only human flight groups excluding myself. - if flight.ai==false and flight.groupname~=playerData.groupname then - - -- Distance to other group. - local distance=flight.group:GetCoordinate():Get2DDistance(mycoord) - - if distance<200 then - table.insert(playerData.section, flight) + -- Remove player from his old section lead. + local removed=self:_RemoveSectionMember(lead, playerData) + if removed then + self:MessageToPlayer(lead, string.format("Flight %s has been removed from your section.", playerData.name), "AIRBOSS", "", 5) + self:MessageToPlayer(playerData, string.format("You have been removed from %s's section.", lead.name), "AIRBOSS", "", 5) end end end - - -- Info on section members. - if #playerData.section>0 then - text=string.format("Registered flight section:") - text=text..string.format("\n- %s (lead)", playerData.name) - for _,_flight in pairs(playerData.section) do - local flight=_flight --#AIRBOSS.PlayerData - text=text..string.format("\n- %s", flight.name) - flight.seclead=playerData.name - -- Inform player that he is now part of a section. - self:MessageToPlayer(flight, string.format("Your section lead is now %s.", playerData.name), "MARSHAL") + -- Potential section members. + local section={} + + -- Loop over all registered flights. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Only human flight groups excluding myself. Also only flights that dont have a section itself (would get messy) or are part of another section (no double membership). + if flight.ai==false and flight.groupname~=playerData.groupname and #flight.section==0 and flight.seclead==flight.name then + + -- Distance (3D) to other flight group. + local distance=flight.group:GetCoordinate():Get3DDistance(mycoord) + + -- Check distance. + if distance remove it. + if not gotit then + self:MessageToPlayer(flight, string.format("you were removed from %s's section and are on your own now.", playerData.name), "AIRBOSS", "", 5) + flight.seclead=flight.name + self:_RemoveSectionMember(playerData, flight) + end + end + + -- Remove all flights that are currently in the player's section already from scanned potential new section members. + for i,_new in pairs(section) do + local newflight=_new.flight --#AIRBOSS.PlayerData + for _,_flight in pairs(playerData.section) do + local currentflight=_flight --#AIRBOSS.PlayerData + if newflight.name==currentflight.name then + table.remove(section, i) + end + end + end + + -- Init section table. Should not be necessary as all members are removed anyhow above. + --playerData.section={} + + -- Output text. + text=string.format("Registered flight section:") + text=text..string.format("\n- %s (lead)", playerData.seclead) + -- Old members that stay (if any). + for _,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + text=text..string.format("\n- %s", flight.name) + end + -- New members (if any). + for i=1,math.min(self.NmaxSection-#playerData.section, #section) do + local flight=section[i].flight --#AIRBOSS.PlayerData + + -- New flight members. + text=text..string.format("\n- %s", flight.name) + + -- Set section lead of player flight. + flight.seclead=playerData.name + + -- Set case of f + flight.case=playerData.case + + -- Inform player that he is now part of a section. + self:MessageToPlayer(flight, string.format("your section lead is now %s.", playerData.name), "AIRBOSS") + + -- Add flight to section table. + table.insert(playerData.section, flight) + end + + -- Section is empty. + if #playerData.section==0 then + text=text..string.format("\n- No other human flights found within radius of %.1f meters!", dmax) + end + end -- Message to section lead. - self:MessageToPlayer(playerData, text, "MARSHAL") + self:MessageToPlayer(playerData, text, "MARSHAL") end end - end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -9959,12 +11138,13 @@ function AIRBOSS:_DisplayScoreBoard(_unitName) for _,_grade in pairs(playerGrades) do local grade=_grade --#AIRBOSS.LSOgrade - -- Add up points if >=0. For foul deck WO we give -1 and it does not count. + -- Add up only final scores for the average. if grade.finalscore then --grade.points>=0 then Paverage=Paverage+grade.finalscore n=n+1 else - --TODO: handle case when the player just leaves after an unfinished pass, e.g bolter, without landing. + -- Case when the player just leaves after an unfinished pass, e.g bolter, without landing. + -- But this should now be solved by deleteing all unfinished results. end end @@ -9992,7 +11172,7 @@ function AIRBOSS:_DisplayScoreBoard(_unitName) local grade=_grade --#AIRBOSS.LSOgrade if grade.finalscore then text=text..string.format("%.1f|", grade.points) - else + elseif grade.points>=0 then -- Only points >=0 as foul deck gives -1. text=text..string.format("(%.1f)", grade.points) end end @@ -10169,19 +11349,19 @@ function AIRBOSS:_DisplayQueue(_unitname, queue, qname) local Charlie=UTILS.SecondsToClock(charlie) local stack=flight.flag:Get() local angels=self:_GetAngels(self:_GetMarshalAltitude(stack, flight.case)) - local nunit=flight.nunits+#flight.section + local _,nunit,nsec=self:_GetFlightUnits(flight, true) local nick=self:_GetACNickname(flight.actype) N=N+nunit - text=text..string.format("\n[Stack %d] %s (%s*%d): Case %d, Angels %d, Charlie %s", stack, flight.onboard, nick, nunit, flight.case, angels, tostring(Charlie)) + text=text..string.format("\n[Stack %d] %s (%s*%d+%d): Case %d, Angels %d, Charlie %s", stack, flight.onboard, nick, nunit, nsec, flight.case, angels, tostring(Charlie)) end - elseif qname=="Pattern" then + elseif qname=="Pattern" or qname=="Waiting" then for i,_flight in pairs(queue) do local flight=_flight --#AIRBOSS.FlightGroup - local nunit=flight.nunits+#flight.section + local _,nunit,nsec=self:_GetFlightUnits(flight, true) local nick=self:_GetACNickname(flight.actype) local ptime=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) N=N+nunit - text=text..string.format("\n[%d] %s (%s*%d): Case %d, T=%s", i, flight.onboard, nick, nunit, flight.case, ptime) + text=text..string.format("\n[%d] %s (%s*%d+%d): Case %d, T=%s", i, flight.onboard, nick, nunit, nsec, flight.case, ptime) end end text=text..string.format("\nTotal AC: %d (airborne %d)", N, nqueue) @@ -10231,6 +11411,8 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) -- Get groups, units in queues. local Nmarshal,nmarshal=self:_GetQueueInfo(self.Qmarshal, playerData.case) local Npattern,npattern=self:_GetQueueInfo(self.Qpattern) + local Nwaiting,nwaiting=self:_GetQueueInfo(self.Qwaiting) + local Ntotal,ntotal=self:_GetQueueInfo(self.flights) -- Current abs time. local Tabs=timer.getAbsTime() @@ -10275,7 +11457,8 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) if self.case==1 then text=text..string.format("Case %d recovery ops\n", self.case) else - text=text..string.format("Case %d recovery ops (%d° offset)\n", self.case, self.holdingoffset) + local radial=self:GetRadial(self.case, true, true, true) + text=text..string.format("Case %d recovery ops\nMarshal radial %03d°)\n", self.case, radial) end text=text..string.format("BRC %03d°\n", self:GetBRC()) text=text..string.format("FB %03d°\n", self:GetFinalBearing(true)) @@ -10287,9 +11470,11 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) if tankertext then text=text..tankertext.."\n" end - text=text..string.format("# A/C total %d\n", #self.flights) + --text=text..string.format("# A/C total %d\n", #self.flights) + text=text..string.format("# A/C total %d (%d)\n", Ntotal, ntotal) text=text..string.format("# A/C marshal %d (%d)\n", Nmarshal, nmarshal) text=text..string.format("# A/C pattern %d (%d)\n", Npattern, npattern) + text=text..string.format("# A/C waiting %d (%d)\n", Nwaiting, nwaiting) text=text..string.format(recoverytext) self:T2(self.lid..text) @@ -10355,6 +11540,26 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) 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) + + if self.staticweather then + local clouds, visibility, fog, dust=self:_GetStaticWeather() + text=text..string.format("\nVisibility %.1f NM", UTILS.MetersToNM(visibility)) + text=text..string.format("\nCloud base %d ft", UTILS.MetersToFeet(clouds.base)) + text=text..string.format("\nCloud thickness %d ft", UTILS.MetersToFeet(clouds.thickness)) + text=text..string.format("\nCloud density %d", clouds.density) + text=text..string.format("\nPrecipitation %d", clouds.iprecptns) + if fog then + text=text..string.format("\nFog thickness %d ft", UTILS.MetersToFeet(fog.thickness)) + text=text..string.format("\nFog visibility %d ft", UTILS.MetersToFeet(fog.visibility)) + else + text=text..string.format("\nNo fog") + end + if dust then + text=text..string.format("\nDust density %d", dust) + else + text=text..string.format("\nNo dust") + end + end -- Debug output. self:T2(self.lid..text) @@ -10411,6 +11616,37 @@ function AIRBOSS:_DisplayAttitude(_unitname) end +--- Turn radio subtitles of player on or off +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_SubtitlesOnOff(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.subtitles=not playerData.subtitles + -- Inform player. + local text="" + if playerData.subtitles==true then + text=string.format("subtitiles are now ON.") + elseif playerData.subtitles==false then + text=string.format("subtitiles are now OFF.") + end + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + end + end + +end + + --- Display player status. -- @param #AIRBOSS self -- @param #string _unitName Name of the player unit. @@ -10449,16 +11685,20 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) -- Hint about TACAN bearing. - if playerData.holding~=nil and playerData.case>1 then + if playerData.step==AIRBOSS.PatternStep.HOLDING and playerData.case>1 then -- Get inverse magnetic radial potential offset. local radial=self:GetRadial(playerData.case, true, true, true) - stacktext=stacktext..string.format("Select TACAN %03d°, DME %d NM\n", radial, angels+15) + stacktext=stacktext..string.format("Select TACAN %03d°, %d DME\n", radial, angels+15) end end -- Fuel and fuel state. local fuel=playerData.unit:GetFuel()*100 local fuelstate=self:_GetFuelState(playerData.unit) + + --- + local _,nunitsGround=self:_GetFlightUnits(playerData, true) + local _,nunitsAirborne=self:_GetFlightUnits(playerData, false) -- Player data. local text=string.format("Status of player %s (%s)\n", playerData.name, playerData.callsign) @@ -10473,9 +11713,8 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) text=text..string.format("Fuel State: %.1f lbs/1000 (%.1f %%)\n", fuelstate/1000, fuel) --text=text..string.format("Aircraft: %s\n", self:_GetACNickname(playerData.actype)) --text=text..string.format("Group: %s\n", playerData.group:GetName()) - text=text..string.format("# units: %d (%d/%d)\n", #playerData.group:GetUnits(), playerData.nunits, #playerData.elements) - text=text..string.format("Section Lead: %s\n", tostring(playerData.seclead)) - text=text..string.format("# section: %d", #playerData.section) + text=text..string.format("# units: %d (%d airborne)\n", nunitsGround, nunitsAirborne) + text=text..string.format("Section Lead: %s (%d/%d)", tostring(playerData.seclead), #playerData.section+1, self.NmaxSection+1) for _,_sec in pairs(playerData.section) do local sec=_sec --#AIRBOSS.PlayerData text=text..string.format("\n- %s", sec.name) @@ -10492,7 +11731,7 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) local brc=self:GetBRC() -- Help player to find its way to the initial zone. - text=text..string.format("\nFly heading %03d° for %.1f NM and turn to BRC %03d°", flyhdg, flydist, brc) + text=text..string.format("\nTo Initial: Fly heading %03d° for %.1f NM and turn to BRC %03d°", flyhdg, flydist, brc) elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then @@ -10507,7 +11746,7 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) local hdg=self:GetRadial(playerData.case, true, true, true) -- Help player to find its way to the initial zone. - text=text..string.format("\nFly heading %03d° for %.1f NM and turn to %03d°", flyhdg, flydist, hdg) + text=text..string.format("\nTo Platform: Fly heading %03d° for %.1f NM and turn to %03d°", flyhdg, flydist, hdg) end @@ -10541,19 +11780,33 @@ function AIRBOSS:_MarkMarshalZone(_unitName, flare) if stack>0 then -- Get current holding zone. - local zone=self:_GetZoneHolding(case, stack) + local zoneHolding=self:_GetZoneHolding(case, stack) + + -- Get Case I commence zone at three position. + local zoneThree=self:_GetZoneCommence(case) -- Pattern alitude. local patternalt=self:_GetMarshalAltitude(stack, case) - patternalt=0 + -- Flare and smoke at the ground. + patternalt=5 if flare then - text="Marking marshal zone with WHITE flares." - zone:FlareZone(FLARECOLOR.White, 45, nil, patternalt) + text=text.."Marking Marshal zone with WHITE flares." + zoneHolding:FlareZone(FLARECOLOR.White, 45, nil, patternalt) + + if playerData.case==1 then + text=text.."\nMarking Commence zone with GREEN flares." + zoneThree:FlareZone(FLARECOLOR.Green, 45, nil, patternalt) + end else - text="Marking marshal zone with WHITE smoke." - zone:SmokeZone(SMOKECOLOR.White, 45, patternalt) + text="Marking Marshal zone with WHITE smoke." + zoneHolding:SmokeZone(SMOKECOLOR.White, 45, patternalt) + + if playerData.case==1 then + text=text.."\nMarking Commence zone with GREEN smoke." + zoneThree:SmokeZone(SMOKECOLOR.Green, 45, patternalt) + end end else @@ -10849,7 +12102,7 @@ function AIRBOSS:onbeforeLoad(From, Event, To, path, filename) -- Check io module is available. if not io then - self:E(self.lid.."ERROR: io not desanitized. Can't load player grades.") + self:E(self.lid.."WARNING: io not desanitized. Can't load player grades.") return false end @@ -10877,7 +12130,7 @@ function AIRBOSS:onbeforeLoad(From, Event, To, path, filename) if exists then return true else - self:E(self.lid..string.format("WARNING: Player LSO grades file %s does not exist.", filename), 60) + self:E(self.lid..string.format("WARNING: Player LSO grades file %s does not exist.", filename)) return false end diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 00f5fa520..c4877d704 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -4,7 +4,7 @@ -- -- **Main Features:** -- --- * Regular pattern update with respect to carrier positon. +-- * Regular pattern update with respect to carrier position. -- * No restrictions regarding carrier waypoints and heading. -- * Automatic respawning when tanker runs out of fuel for 24/7 operations. -- * Tanker can be spawned cold or hot on the carrier or at any other airbase or directly in air. @@ -52,7 +52,8 @@ -- @field #boolean uncontrolledac If true, use and uncontrolled tanker group already present in the mission. -- @field DCS#Vec3 orientation Orientation of the carrier. Used to monitor changes and update the pattern if heading changes significantly. -- @field DCS#Vec3 orientlast Orientation of the carrier for checking if carrier is currently turning. --- @field Core.Point#COORDINATE position Positon of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. +-- @field Core.Point#COORDINATE position Position of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. +-- @field #string alias Alias of the spawn group. -- @extends Core.Fsm#FSM --- Recovery Tanker. @@ -100,7 +101,7 @@ -- -- ## Takeoff Type -- --- By default, the tanker is spawned with running engies on the carrier. The mission designer has set option to set the take off type via the @{#RECOVERYTANKER.SetTakeoff} function. +-- By default, the tanker is spawned with running engines on the carrier. The mission designer has set option to set the take off type via the @{#RECOVERYTANKER.SetTakeoff} function. -- Or via shortcuts -- -- * @{#RECOVERYTANKER.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default. @@ -113,7 +114,7 @@ -- TexacoStennis:Start() -- will spawn the tanker several nautical miles astern the carrier. From there it will start its pattern. -- --- Spawning in air is not as realsitic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck. +-- Spawning in air is not as realistic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck. -- -- **Note** that when spawning in air is set, the tanker will also not return to the boat, once it is out of fuel. Instead it will be respawned directly in air. -- @@ -177,7 +178,7 @@ -- * The aircraft carrier changes its heading by more than 5 degrees (see @{#RECOVERYTANKER.SetPatternUpdateHeading}) -- -- **Note** that updating the pattern often leads to a more or less small disruption of the perfect racetrack pattern of the tanker. This is because a new waypoint and new racetrack points --- need to be set as DCS task. This is the reason why the pattern is not contantly updated but rather when the position or heading of the carrier changes significantly. +-- need to be set as DCS task. This is the reason why the pattern is not constantly updated but rather when the position or heading of the carrier changes significantly. -- -- The maximum update frequency is set to 10 minutes. You can adjust this by @{#RECOVERYTANKER.SetPatternUpdateInterval}. -- Also the pattern will not be updated whilst the carrier is turning or the tanker is currently refueling another unit. @@ -250,11 +251,16 @@ RECOVERYTANKER = { orientation = nil, orientlast = nil, position = nil, + alias = nil, } +--- Unique ID (global). +-- @field #number uid Unique ID (global). +RECOVERYTANKER.uid=0 + --- Class version. -- @field #string version -RECOVERYTANKER.version="1.0.2" +RECOVERYTANKER.version="1.0.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -264,7 +270,7 @@ RECOVERYTANKER.version="1.0.2" -- DONE: Seamless change of position update. Get good updated waypoint and update position if tanker position is right. Not really possiple atm. -- DONE: Check if TACAN mode "X" is allowed for AA TACAN stations. Nope -- DONE: Check if tanker is going back to "Running" state after RTB and respawn. --- DONE: Write documenation. +-- DONE: Write documentation. -- DONE: Trace functions self:T instead of self:I for less output. -- DONE: Make pattern update parameters (distance, orientation) input parameters. -- DONE: Add FSM event for pattern update. @@ -302,8 +308,14 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- Save self in static object. Easier to retrieve later. self.carrier:SetState(self.carrier, "RECOVERYTANKER", self) - -- Debug log id. - self.lid=string.format("RECOVERYTANKER %s", self.carrier:GetName()) + -- Increase unique ID. + RECOVERYTANKER.uid=RECOVERYTANKER.uid+1 + + -- Set unique spawn alias. + self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.tankergroupname, RECOVERYTANKER.uid) + + -- Log ID. + self.lid=string.format("RECOVERYTANKER %s |", self.alias) -- Init default parameters. self:SetAltitude() @@ -715,11 +727,8 @@ function RECOVERYTANKER:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Refueling, self._RefuelingStart) --Need explcit functions sice OnEventRefueling and OnEventRefuelingStop did not hook. self:HandleEvent(EVENTS.RefuelingStop, self._RefuelingStop) - -- Set unique alias for spawn from tanker group name and carrier unit name. - local tankergroupalias=string.format("%s_%s", self.tankergroupname, self.carrier:GetName()) - -- Spawn tanker. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. - local Spawn=SPAWN:NewWithAlias(self.tankergroupname, tankergroupalias) + local Spawn=SPAWN:NewWithAlias(self.tankergroupname, self.alias) -- Set radio frequency and modulation. Spawn:InitRadioCommsOnOff(true) @@ -774,7 +783,6 @@ function RECOVERYTANKER:onafterStart(From, Event, To) -- Initialize route. self.distStern<0! SCHEDULER:New(nil, self._InitRoute, {self, -self.distStern+UTILS.NMToMeters(3)}, 1) - --self:_InitRoute(-self.distStern+UTILS.NMToMeters(3), 1) -- Create tanker beacon. if self.TACANon then @@ -801,79 +809,102 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) -- Get current time. local time=timer.getTime() - -- Get fuel of tanker. - local fuel=self.tanker:GetFuel()*100 - local text=string.format("Recovery tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) - self:T(self.lid..text) + if self.tanker:IsAlive() then - -- Check if tanker is running and not RTBing or refueling. - if self:IsRunning() then + --------------------- + -- TANKER is ALIVE -- + --------------------- - -- Check fuel. - if fuel0 starboard, z<0 port. Default 0 m. +-- @return Core.Point#COORDINATE The COORDINATE of the offset with respect to the orientation of the POSITIONABLE. +function POSITIONABLE:GetOffsetCoordinate(x,y,z) + + -- Default if nil. + x=x or 0 + y=y or 0 + z=z or 0 + + -- Vectors making up the coordinate system. + local X=self:GetOrientationX() + local Y=self:GetOrientationY() + local Z=self:GetOrientationZ() + + -- Offset vector: x meters ahead, z meters starboard, y meters above. + local A={x=x, y=y, z=z} + + -- Scale components of orthonormal coordinate vectors. + local x={x=X.x*A.x, y=X.y*A.x, z=X.z*A.x} + local y={x=Y.x*A.y, y=Y.y*A.y, z=Y.z*A.y} + local z={x=Z.x*A.z, y=Z.y*A.z, z=Z.z*A.z} + + -- Add up vectors in the unit coordinate system ==> this gives the offset vector relative the the origin of the map. + local a={x=x.x+y.x+z.x, y=x.y+y.y+z.y, z=x.z+y.z+z.z} + + -- Vector from the origin of the map to the unit. + local u=self:GetVec3() + + -- Translate offset vector from map origin to the unit: v=u+a. + local v={x=a.x+u.x, y=a.y+u.y, z=a.z+u.z} + + -- Return the offset coordinate. + return COORDINATE:NewFromVec3(v) +end --- Returns a random @{DCS#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. -- @param Wrapper.Positionable#POSITIONABLE self diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 16278596a..4bbcaac95 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -783,6 +783,27 @@ function UNIT:GetThreatLevel() end +--- Triggers an explosion at the coordinates of the unit. +-- @param #UNIT self +-- @param #number power Power of the explosion in kg TNT. Default 100 kg TNT. +-- @param #number delay (Optional) Delay of explosion in seconds. +-- @return #UNIT self +function UNIT:Explode(power, delay) + + -- Default. + power=power or 100 + + -- Check if delay or not. + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(nil, self.Explode, {self, power}, delay) + else + -- Create an explotion at the coordinate of the unit. + self:GetCoordinate():Explosion(power) + end + + return self +end -- Is functions From 5a5340431e518e70cb9af4f4b39f748f2e0ac049 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 20 Jan 2019 17:03:18 +0100 Subject: [PATCH 136/147] Improvements ... -- Implementation of queueing of aircraft launches. -- Fixed the RTB bug resulting in "lost control". -- Communication of AI to players (first version). -- Aircraft engage distance calculation for each wave. --- Moose Development/Moose/AI/AI_A2G_BAI.lua | 3 +- Moose Development/Moose/AI/AI_A2G_CAS.lua | 1 - .../Moose/AI/AI_A2G_Dispatcher.lua | 819 ++++++++++++++---- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 5 +- Moose Development/Moose/AI/AI_Air.lua | 28 +- .../Moose/Tasking/DetectionManager.lua | 28 + 6 files changed, 726 insertions(+), 158 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index 736dbcf98..5431a042e 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -106,9 +106,10 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit self:SetTargetDistance( ToCoord ) -- For RTB status check local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) --- Create a route point of type air. - local ToWP = ToCoord:Translate( 10000, FromEngageAngle ):WaypointAir( + local ToWP = ToCoord:Translate( EngageDistance, FromEngageAngle ):WaypointAir( self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 8cc974b7a..d3f565ce1 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -105,7 +105,6 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit self:SetTargetDistance( TargetCoord ) -- For RTB status check local FromEngageAngle = TargetCoord:GetAngleDegrees( TargetCoord:GetDirectionVec3( DefenderCoord ) ) - local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) --- Create a route point of type air. diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 34988ce7d..37f96023f 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -305,7 +305,7 @@ do -- AI_A2G_DISPATCHER -- -- An reconnaissance network, is used to detect enemy ground targets, potentially group them into areas, and to understand the position, level of threat of the enemy. -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia5.JPG) + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia5.JPG) -- -- As explained in the introduction, depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets. -- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range. @@ -493,9 +493,9 @@ do -- AI_A2G_DISPATCHER -- -- You need to configure each squadron which task types you want it to perform. Read on ... -- - -- ### 3.2. Squadrons enemy ground target **Engagement**. + -- ### 3.2. Squadrons enemy ground target **engagement types**. -- - -- There are two ways how targets can be engaged: directly upon call from the airfield, farp or carrier, or through a patrol. + -- There are two ways how targets can be engaged: directly **on call** from the airfield, farp or carrier, or through a **patrol**. -- -- Patrols are extremely handy, as these will airborne your helicopters or airplanes in advance. They will patrol in defined zones outlined, -- and will engage with the targets once commanded. If the patrol zone is close enough to the enemy ground targets, then the time required @@ -505,7 +505,7 @@ do -- AI_A2G_DISPATCHER -- -- The mission designer needs to carefully balance the need for patrols or the need for engagement on call from the airfields. -- - -- ### 3.3. Squadron **on call engagement**. + -- ### 3.3. Squadron **on call** engagement. -- -- So to make squadrons engage targets from the airfields, use the following methods: -- @@ -558,12 +558,350 @@ do -- AI_A2G_DISPATCHER -- A2GDispatcher:SetSquadronBaiPatrol( "Maykop BAI", PatrolZone, 800, 900, 50, 80, 250, 300 ) -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop BAI", 2, 30, 60, 1, "BAI" ) -- + -- + -- ### 3.5. Set squadron take-off methods + -- + -- Use the various SetSquadronTakeoff... methods to control how squadrons are taking-off from the home airfield, FARP or ship. + -- + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. + -- + -- **The default landing method is to spawn new aircraft directly in the air.** + -- + -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. + -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: + -- + -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. + -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. + -- * aircraft may collide at the airbase. + -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... + -- + -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. + -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! + -- + -- This example sets the default takeoff method to be from the runway. + -- And for a couple of squadrons overrides this default method. + -- + -- -- Setup the Takeoff methods + -- + -- -- The default takeoff + -- A2ADispatcher:SetDefaultTakeOffFromRunway() + -- + -- -- The individual takeoff per squadron + -- A2ADispatcher:SetSquadronTakeoff( "Mineralnye", AI_A2G_DISPATCHER.Takeoff.Air ) + -- A2ADispatcher:SetSquadronTakeoffInAir( "Sochi" ) + -- A2ADispatcher:SetSquadronTakeoffFromRunway( "Mozdok" ) + -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "Maykop" ) + -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "Novo" ) + -- + -- + -- ### 3.5.1. Set Squadron takeoff altitude when spawning new aircraft in the air. + -- + -- In the case of the @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() there is also an other parameter that can be applied. + -- That is modifying or setting the **altitude** from where planes spawn in the air. + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAirAltitude}() to set the altitude for a specific squadron. + -- The default takeoff altitude can be modified or set using the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAirAltitude}(). + -- As part of the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() a parameter can be specified to set the takeoff altitude. + -- If this parameter is not specified, then the default altitude will be used for the squadron. + -- + -- ### 3.5.2. Set Squadron takeoff interval. + -- + -- The different types of available airfields have different amounts of available launching platforms: + -- + -- - Airbases typically have a lot of platforms. + -- - FARPs have 4 platforms. + -- - Ships have 2 to 4 platforms. + -- + -- Depending on the demand of requested takeoffs by the A2G dispatcher, an airfield can become overloaded. Too many aircraft need to be taken + -- off at the same time, which will result in clutter as described above. In order to better control this behaviour, a takeoff scheduler is implemented, + -- which can be used to control how many aircraft are ordered for takeoff between specific time intervals. + -- The takeff intervals can be specified per squadron, which make sense, as each squadron have a "home" airfield. + -- + -- For this purpose, the method @{#AI_A2G_DISPATCHER.SetSquadronTakeOffInterval}() can be used to specify the takeoff intervals of + -- aircraft groups per squadron to avoid cluttering of aircraft at airbases. + -- This is especially useful for FARPs and ships. Each takeoff dispatch is queued by the dispatcher and when the interval time + -- has been reached, a new group will be spawned or activated for takeoff. + -- + -- The interval needs to be estimated, and depends on the time needed for the aircraft group to actually depart from the launch platform, and + -- the way how the aircraft are starting up. Cold starts take the longest duration, hot starts a few seconds, and runway takeoff also a few seconds for FARPs and ships. + -- + -- See the underlying example: + -- + -- -- Imagine a squadron launched from a FARP, with a grouping of 4. + -- -- Aircraft will cold start from the FARP, and thus, a maximum of 4 aircraft can be launched at the same time. + -- -- Additionally, depending on the group composition of the aircraft, defending units will be ordered for takeoff together. + -- -- It takes about 3 to 4 minutes to takeoff helicopters from FARPs in cold start. + -- A2ADispatcher:SetSquadronTakeOffInterval( "Mineralnye", 60 * 4 ) + -- + -- + -- ### 3.6. Set squadron landing methods + -- + -- In analogy with takeoff, the landing methods are to control how squadrons land at the airfield: + -- + -- * @{#AI_A2G_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. + -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the + -- A2A defense system, as no new CAP or GCI planes can takeoff. + -- Note that the method @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. + -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. + -- + -- This example defines the default landing method to be at the runway. + -- And for a couple of squadrons overrides this default method. + -- + -- -- Setup the Landing methods + -- + -- -- The default landing method + -- A2ADispatcher:SetDefaultLandingAtRunway() + -- + -- -- The individual landing per squadron + -- A2ADispatcher:SetSquadronLandingAtRunway( "Mineralnye" ) + -- A2ADispatcher:SetSquadronLandingNearAirbase( "Sochi" ) + -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "Mozdok" ) + -- A2ADispatcher:SetSquadronLandingNearAirbase( "Maykop" ) + -- A2ADispatcher:SetSquadronLanding( "Novo", AI_A2G_DISPATCHER.Landing.AtRunway ) + -- + -- + -- ### 3.7. Set squadron **grouping**. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronGrouping}() to set the grouping of aircraft when spawned in. + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia12.JPG) + -- + -- In the case of **on call** engagement, the @{#AI_A2G_DISPATCHER.SetSquadronGrouping}() method has additional behaviour. + -- When there aren't enough patrol flights airborne, a on call will be initiated for the remaining + -- targets to be engaged. Depending on the grouping parameter, the spawned flights for on call aircraft are grouped into this setting. + -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by the available patrols or any airborne flight, + -- an additional on call flight needs to be started. + -- + -- The **grouping value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense flights grouping when the tactical situation changes. + -- + -- ### 3.8. Set the squadron **overhead** to balance the effectiveness of the A2G defenses. + -- + -- The effectiveness can be set with the **overhead parameter**. This is a number that is used to calculate the amount of Units that dispatching command will allocate to GCI in surplus of detected amount of units. + -- The **default value** of the overhead parameter is 1.0, which means **equal balance**. + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia11.JPG) + -- + -- However, depending on the (type of) aircraft (strength and payload) in the squadron and the amount of resources available, this parameter can be changed. + -- + -- The @{#AI_A2G_DISPATCHER.SetSquadronOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. + -- + -- For example, a A-10C with full long-distance A2G missiles payload, may still be less effective than a Su-23 with short range A2G missiles... + -- So in this case, one may want to use the @{#AI_A2G_DISPATCHER.SetOverhead}() method to allocate more defending planes as the amount of detected attacking ground units. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that overhead values: + -- + -- * Higher than 1.0, for example 1.5, will increase the defense unit amounts. For 4 attacking ground units detected, 6 aircraft will be spawned. + -- * Lower than 1, for example 0.75, will decrease the defense unit amounts. For 4 attacking ground units detected, only 3 aircraft will be spawned. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking ground units as part of the detected group + -- multiplied by the overhead parameter, and rounded up to the smallest integer. + -- + -- Typically, for A2G defenses, values small than 1 will be used. Here are some good values for a couple of aircraft to support CAS operations: + -- + -- - A-10C: 0.15 + -- - Su-34: 0.15 + -- - A-10A: 0.25 + -- - SU-25T: 0.10 + -- + -- So generically, the amount of missiles that an aircraft can take will determine its attacking effectiveness. The longer the range of the missiles, + -- the less risk that the defender may be destroyed by the enemy, thus, the less aircraft needs to be activated in a defense. + -- + -- The **overhead value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense overhead when the tactical situation changes. + -- + -- ### 3.8. Set the squadron **engage limit**. + -- + -- To limit the amount of aircraft to defend against a large group of intruders, an **engage limit** can be defined per squadron. + -- This limit will avoid an extensive amount of aircraft to engage with the enemy if the attacking ground forces are enormous. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronEngageLimit}() to limit the amount of aircraft that will engage with the enemy, per squadron. + -- + -- ## 4. Set the **fuel treshold**. + -- + -- When aircraft get **out of fuel** to a certain %-tage, which is by default **15% (0.15)**, there are two possible actions that can be taken: + -- - The aircraft will go RTB, and will be replaced with a new aircraft if possible. + -- - The aircraft will refuel at a tanker, if a tanker has been specified for the squadron. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel treshold** of the aircraft for all squadrons. + -- + -- ## 6. Other configuration options + -- + -- ### 6.1. Set a tactical display panel. + -- + -- Every 30 seconds, a tactical display panel can be shown that illustrates what the status is of the different groups controlled by AI_A2G_DISPATCHER. + -- Use the method @{#AI_A2G_DISPATCHER.SetTacticalDisplay}() to switch on the tactical display panel. The default will not show this panel. + -- Note that there may be some performance impact if this panel is shown. + -- + -- ## 10. Default settings. + -- + -- Default settings configure the standard behaviour of the squadrons. + -- This section a good overview of the different parameters that setup the behaviour of **ALL** the squadrons by default. + -- Note that default behaviour can be tweaked, and thus, this will change the behaviour of all the squadrons. + -- Unless there is a specific behaviour set for a specific squadron, the default configured behaviour will be followed. + -- + -- ## 10.1. Default **takeoff** behaviour. + -- + -- The default takeoff behaviour is set to **in the air**, which means that new spawned aircraft will be spawned directly in the air above the airbase by default. + -- + -- **The default takeoff method can be set for ALL squadrons that don't have an individual takeoff method configured.** + -- + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoff}() is the generic configuration method to control takeoff by default from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffInAir}() will spawn by default new aircraft from the squadron directly in the air. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromParkingCold}() will spawn by default new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromParkingHot}() will spawn by default new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromRunway}() will spawn by default new aircraft at the runway at the airfield. + -- + -- ## 10.2. Default landing behaviour. + -- + -- The default landing behaviour is set to **near the airbase**, which means that returning airplanes will be despawned directly in the air by default. + -- + -- The default landing method can be set for ALL squadrons that don't have an individual landing method configured. + -- + -- * @{#AI_A2G_DISPATCHER.SetDefaultLanding}() is the generic configuration method to control by default landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_A2G_DISPATCHER.SetDefaultLandingNearAirbase}() will despawn by default the returning aircraft in the air when near the airfield. + -- * @{#AI_A2G_DISPATCHER.SetDefaultLandingAtRunway}() will despawn by default the returning aircraft directly after landing at the runway. + -- * @{#AI_A2G_DISPATCHER.SetDefaultLandingAtEngineShutdown}() will despawn by default the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- ## 10.3. Default **overhead**. + -- + -- The default overhead is set to **0.25**. That essentially means that for each 4 ground enemies there will be 1 aircraft dispatched. + -- + -- The default overhead value can be set for ALL squadrons that don't have an individual overhead value configured. + -- + -- Use the @{#AI_A2G_DISPATCHER.SetDefaultOverhead}() method can be used to set the default overhead or defense strength for ALL squadrons. + -- + -- ## 10.4. Default **grouping**. + -- + -- The default grouping is set to **one airplane**. That essentially means that there won't be any grouping applied by default. + -- + -- The default grouping value can be set for ALL squadrons that don't have an individual grouping value configured. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultGrouping}() to set the **default grouping** of spawned airplanes for all squadrons. + -- + -- ## 10.5. Default RTB fuel treshold. + -- + -- When an airplane gets **out of fuel** to a certain %-tage, which is **15% (0.15)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel treshold** of spawned airplanes for all squadrons. + -- + -- ## 10.6. Default RTB damage treshold. + -- + -- When an airplane is **damaged** to a certain %-tage, which is **40% (0.40)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage treshold** of spawned airplanes for all squadrons. + -- + -- ## 10.7. Default settings for **patrol**. + -- + -- ### 10.7.1. Default **patrol time Interval**. + -- + -- Patrol dispatching is time event driven, and will evaluate in random time intervals if a new patrol needs to be dispatched. + -- + -- The default patrol time interval is between **180** and **600** seconds. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultPatrolTimeInterval}() to set the **default patrol time interval** of dispatched aircraft for ALL squadrons. + -- + -- Note that you can still change the patrol limit and patrol time intervals for each patrol individually using + -- the @{#AI_A2G_DISPATCHER.SetSquadronPatrolTimeInterval}() method. + -- + -- ### 10.7.2. Default **patrol limit**. + -- + -- Multiple patrol can be airborne at the same time for one squadron, which is controlled by the **patrol limit**. + -- The **default patrol limit** is 1 patrol per squadron to be airborne at the same time. + -- Note that the default patrol limit is used when a squadron patrol is defined, and cannot be changed afterwards. + -- So, ensure that you set the default patrol limit **before** you define or setup the squadron patrol. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultPatrolTimeInterval}() to set the **default patrol time interval** of dispatched aircraft patrols for all squadrons. + -- Note that you can still change the patrol limit and patrol time intervals for each patrol individually using + -- the @{#AI_A2G_DISPATCHER.SetSquadronPatrolTimeInterval}() method. + -- + -- ## 10.7.3. Default tanker for refuelling when executing CAP. + -- + -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker. + -- This greatly increases the efficiency of your CAP operations. + -- + -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected. + -- Then, use the method @{#AI_A2G_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher. + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the %-tage left in the defender airplane tanks when a refuel action is needed. + -- + -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. + -- + -- For example, the following setup will set the default refuel tanker to "Tanker": + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_DISPATCHER-ME_11.JPG) + -- + -- -- Define the CAP + -- A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-34" }, 20 ) + -- A2ADispatcher:SetSquadronCap( "Sochi", ZONE:New( "PatrolZone" ), 4000, 8000, 600, 800, 1000, 1300 ) + -- A2ADispatcher:SetSquadronCapInterval("Sochi", 2, 30, 600, 1 ) + -- A2ADispatcher:SetSquadronGci( "Sochi", 900, 1200 ) + -- + -- -- Set the default tanker for refuelling to "Tanker", when the default fuel treshold has reached 90% fuel left. + -- A2ADispatcher:SetDefaultFuelThreshold( 0.9 ) + -- A2ADispatcher:SetDefaultTanker( "Tanker" ) + -- + -- ## 10.8. Default settings for GCI. + -- + -- ## 10.8.1. Optimal intercept point calculation. + -- + -- When intruders are detected, the intrusion path of the attackers can be monitored by the EWR. + -- Although defender planes might be on standby at the airbase, it can still take some time to get the defenses up in the air if there aren't any defenses airborne. + -- This time can easily take 2 to 3 minutes, and even then the defenders still need to fly towards the target, which takes also time. + -- + -- Therefore, an optimal **intercept point** is calculated which takes a couple of parameters: + -- + -- * The average bearing of the intruders for an amount of seconds. + -- * The average speed of the intruders for an amount of seconds. + -- * An assumed time it takes to get planes operational at the airbase. + -- + -- The **intercept point** will determine: + -- + -- * If there are any friendlies close to engage the target. These can be defenders performing CAP or defenders in RTB. + -- * The optimal airbase from where defenders will takeoff for GCI. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetIntercept}() to modify the assumed intercept delay time to calculate a valid interception. + -- + -- ## 10.8.2. Default Disengage Radius. + -- + -- The radius to **disengage any target** when the **distance** of the defender to the **home base** is larger than the specified meters. + -- The default Disengage Radius is **300km** (300000 meters). Note that the Disengage Radius is applicable to ALL squadrons! + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDisengageRadius}() to modify the default Disengage Radius to another distance setting. + -- + -- ## 11. Airbase capture: + -- + -- Different squadrons can be located at one airbase. + -- If the airbase gets captured, that is, when there is an enemy unit near the airbase, and there aren't anymore friendlies at the airbase, the airbase will change coalition ownership. + -- As a result, the GCI and CAP will stop! + -- However, the squadron will still stay alive. Any airplane that is airborne will continue its operations until all airborne airplanes + -- of the squadron will be destroyed. This to keep consistency of air operations not to confuse the players. + -- + -- + -- + -- -- @field #AI_A2G_DISPATCHER AI_A2G_DISPATCHER = { ClassName = "AI_A2G_DISPATCHER", Detection = nil, } + --- Definition of a Squadron. + -- @type AI_A2G_DISPATCHER.Squadron + -- @field #string Name The Squadron name. + -- @field Wrapper.Airbase#AIRBASE Airbase The home airbase. + -- @field #string AirbaseName The name of the home airbase. + -- @field Core.Spawn#SPAWN Spawn The spawning object. + -- @field #number ResourceCount The number of resources available. + -- @field #list<#string> TemplatePrefixes The list of template prefixes. + -- @field #boolean Captured true if the squadron is captured. + -- @field #number Overhead The overhead for the squadron. + --- List of defense coordinates. -- @type AI_A2G_DISPATCHER.DefenseCoordinates @@ -587,6 +925,32 @@ do -- AI_A2G_DISPATCHER AtEngineShutdown = 3, } + --- A defense queue item description + -- @type AI_A2G_DISPATCHER.DefenseQueueItem + -- @field Squadron + -- @field #AI_A2G_DISPATCHER.Squadron DefenderSquadron The squadron in the queue. + -- @field DefendersNeeded + -- @field Defense + -- @field DefenseTaskType + -- @field Functional.Detection#DETECTION_BASE AttackerDetection + -- @field DefenderGrouping + -- @field #string SquadronName The name of the squadron. + + --- Queue of planned defenses to be launched. + -- This queue exists because defenses must be launched on FARPS, or in the air, or on an airbase, or on carriers. + -- And some of these platforms have very limited amount of "launching" platforms. + -- Therefore, this queue concept is introduced that queues each defender request. + -- Depending on the location of the launching site, the queued defenders will be launched at varying time intervals. + -- This guarantees that launched defenders are also directly existing ... + -- @type AI_A2G_DISPATCHER.DefenseQueue + -- @list<#AI_A2G_DISPATCHER.DefenseQueueItem> DefenseQueueItem A list of all defenses being queued ... + + --- @field #AI_A2G_DISPATCHER.DefenseQueue DefenseQueue + AI_A2G_DISPATCHER.DefenseQueue = {} + + + + --- AI_A2G_DISPATCHER constructor. -- This is defining the A2G DISPATCHER for one coaliton. -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units. @@ -759,6 +1123,8 @@ do -- AI_A2G_DISPATCHER self:SetDefenseReactivityMedium() + self.TakeoffScheduleID = self:ScheduleRepeat( 10, 10, 0, nil, self.ResourceTakeoff, self ) + self:__Start( 5 ) return self @@ -774,14 +1140,14 @@ do -- AI_A2G_DISPATCHER for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do DefenderSquadron.Resource = {} for Resource = 1, DefenderSquadron.ResourceCount or 0 do - self:ParkDefender( DefenderSquadron ) + self:ResourcePark( DefenderSquadron ) end end end --- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:ParkDefender( DefenderSquadron ) + function AI_A2G_DISPATCHER:ResourcePark( DefenderSquadron ) local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN Spawn:InitGrouping( 1 ) @@ -838,7 +1204,7 @@ do -- AI_A2G_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() - self:ParkDefender( Squadron, Defender ) + self:ResourcePark( Squadron, Defender ) return end if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then @@ -865,11 +1231,11 @@ do -- AI_A2G_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() - self:ParkDefender( Squadron, Defender ) + self:ResourcePark( Squadron, Defender ) end end end - + do -- Manage the defensive behaviour --- @param #AI_A2G_DISPATCHER self @@ -1352,7 +1718,6 @@ do -- AI_A2G_DISPATCHER -- @return #AI_A2G_DISPATCHER function AI_A2G_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} local DefenderSquadron = self.DefenderSquadrons[SquadronName] @@ -1379,6 +1744,8 @@ do -- AI_A2G_DISPATCHER DefenderSquadron.TemplatePrefixes = TemplatePrefixes DefenderSquadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured. + self:SetSquadronTakeoffInterval( SquadronName, 0 ) + self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) return self @@ -1449,6 +1816,28 @@ do -- AI_A2G_DISPATCHER end + --- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number TakeoffInterval Only Takeoff new units each specified interval in seconds in 10 seconds steps. + -- @usage + -- + -- -- Set the Squadron Takeoff interval every 60 seconds for squadron "SQ50", which is good for a FARP cold start. + -- A2GDispatcher:SetSquadronTakeoffInterval( "SQ50", 60 ) + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffInterval( SquadronName, TakeoffInterval ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + DefenderSquadron.TakeoffInterval = TakeoffInterval or 0 + DefenderSquadron.TakeoffTime = 0 + end + + end + + --- Set the squadron patrol parameters for a specific task type. -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. @@ -2806,6 +3195,7 @@ do -- AI_A2G_DISPATCHER if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then local SquadronOverhead = self:GetSquadronOverhead( DefenderSquadronName ) + self:F( { SquadronOverhead = SquadronOverhead } ) if DefenderSize then DefendersEngaged = DefendersEngaged + DefenderSize DefendersMissing = DefendersMissing - DefenderSize / SquadronOverhead @@ -2820,6 +3210,15 @@ do -- AI_A2G_DISPATCHER end + for QueueID, QueueItem in pairs( self.DefenseQueue ) do + local QueueItem = QueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem + if QueueItem.AttackerDetection and QueueItem.AttackerDetection.ItemID == AttackerDetection.ItemID then + DefendersMissing = DefendersMissing - QueueItem.DefendersNeeded / QueueItem.DefenderSquadron.Overhead + --DefendersEngaged = DefendersEngaged + QueueItem.DefenderGrouping + end + self:F( { QueueItemName = QueueItem.Defense, QueueItem_ItemID = QueueItem.AttackerDetection.ItemID, DetectedItem = AttackerDetection.ItemID, DefendersMissing = DefendersMissing } ) + end + self:F( { DefenderCount = DefendersEngaged } ) return DefendersTotal, DefendersEngaged, DefendersMissing @@ -2864,7 +3263,7 @@ do -- AI_A2G_DISPATCHER break end end - + return Friendlies end @@ -2950,72 +3349,263 @@ do -- AI_A2G_DISPATCHER -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:onafterPatrol( From, Event, To, SquadronName, DefenseTaskType ) - self:F({SquadronName = SquadronName}) - local DefenderSquadron, Patrol = self:CanPatrol( SquadronName, DefenseTaskType ) if Patrol then - - local DefenderPatrol, DefenderGrouping = self:ResourceActivate( DefenderSquadron ) - - if DefenderPatrol then - - local AI_A2G_PATROL = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } - - local Fsm = AI_A2G_PATROL[DefenseTaskType]:New( DefenderPatrol, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.EngageFloorAltitude, Patrol.EngageCeilingAltitude, Patrol.Zone, Patrol.PatrolFloorAltitude, Patrol.PatrolCeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.AltType ) - Fsm:SetDispatcher( self ) - Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) - Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) - Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) - Fsm:SetDisengageRadius( self.DisengageRadius ) - Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) - Fsm:Start() - - self:SetDefenderTask( SquadronName, DefenderPatrol, DefenseTaskType, Fsm, nil, DefenderGrouping ) - - function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"Patrol Birth", Defender:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - - if Squadron then - Fsm:__Patrol( 2 ) -- Start Patrolling - end - end - - function Fsm:onafterRTB( Defender, From, Event, To ) - self:F({"Patrol RTB", Defender:GetName()}) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) - local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - Dispatcher:ClearDefenderTaskTarget( Defender ) - end - - --- @param #AI_A2G_DISPATCHER self - function Fsm:onafterHome( Defender, From, Event, To, Action ) - self:F({"Patrol Home", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - - local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - - if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - - if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - Dispatcher:ParkDefender( Squadron, Defender ) - end - end - end + self:ResourceQueue( true, DefenderSquadron, nil, Patrol, DefenseTaskType, nil, SquadronName ) end end + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceQueue( Patrol, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName ) + + self:F( { DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName } ) + + local DefenseQueueItem = {} -- #AI_A2G_DISPATCHER.DefenderQueueItem + + + DefenseQueueItem.Patrol = Patrol + DefenseQueueItem.DefenderSquadron = DefenderSquadron + DefenseQueueItem.DefendersNeeded = DefendersNeeded + DefenseQueueItem.Defense = Defense + DefenseQueueItem.DefenseTaskType = DefenseTaskType + DefenseQueueItem.AttackerDetection = AttackerDetection + DefenseQueueItem.SquadronName = SquadronName + + table.insert( self.DefenseQueue, DefenseQueueItem ) + self:F( { QueueItems = #self.DefenseQueue } ) + + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceTakeoff() + + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + self:F( { DefenseQueueID } ) + end + + for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do + + if #self.DefenseQueue > 0 then + + self:F( { SquadronName, Squadron.Name, Squadron.TakeoffTime, Squadron.TakeoffInterval, timer.getTime() } ) + + local DefenseQueueItem = self.DefenseQueue[1] + self:F( {DefenderSquadron=DefenseQueueItem.DefenderSquadron} ) + + if DefenseQueueItem.SquadronName == SquadronName then + + if Squadron.TakeoffTime + Squadron.TakeoffInterval < timer.getTime() then + Squadron.TakeoffTime = timer.getTime() + + if DefenseQueueItem.Patrol == true then + self:ResourcePatrol( DefenseQueueItem.DefenderSquadron, DefenseQueueItem.DefendersNeeded, DefenseQueueItem.Defense, DefenseQueueItem.DefenseTaskType, DefenseQueueItem.AttackerDetection, DefenseQueueItem.SquadronName ) + else + self:ResourceEngage( DefenseQueueItem.DefenderSquadron, DefenseQueueItem.DefendersNeeded, DefenseQueueItem.Defense, DefenseQueueItem.DefenseTaskType, DefenseQueueItem.AttackerDetection, DefenseQueueItem.SquadronName ) + end + table.remove( self.DefenseQueue, 1 ) + end + end + end + + end + + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourcePatrol( DefenderSquadron, DefendersNeeded, Patrol, DefenseTaskType, AttackerDetection, SquadronName ) + + + self:F({DefenderSquadron=DefenderSquadron}) + self:F({DefendersNeeded=DefendersNeeded}) + self:F({Patrol=Patrol}) + self:F({DefenseTaskType=DefenseTaskType}) + self:F({AttackerDetection=AttackerDetection}) + self:F({SquadronName=SquadronName}) + + local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + if DefenderGroup then + + local AI_A2G_PATROL = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + + local Fsm = AI_A2G_PATROL[DefenseTaskType]:New( DefenderGroup, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.EngageFloorAltitude, Patrol.EngageCeilingAltitude, Patrol.Zone, Patrol.PatrolFloorAltitude, Patrol.PatrolCeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.AltType ) + Fsm:SetDispatcher( self ) + Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + Fsm:SetDisengageRadius( self.DisengageRadius ) + Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) + Fsm:Start() + + self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, Fsm, nil, DefenderGrouping ) + + function Fsm:onafterTakeoff( Defender, From, Event, To ) + self:F({"Defender Birth", Defender:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + + if Squadron then + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne." ) + Fsm:Patrol() -- Engage on the TargetSetUnit + end + end + + function Fsm:onafterRTB( Defender, From, Event, To ) + self:F({"Defender RTB", Defender:GetName()}) + self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) + + Dispatcher:ClearDefenderTaskTarget( Defender ) + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterLostControl( Defender, From, Event, To ) + self:F({"Defender LostControl", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) + if Defender:IsAboveRunway() then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterHome( Defender, From, Event, To, Action ) + self:F({"Defender Home", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) + + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + Dispatcher:ResourcePark( Squadron, Defender ) + end + end + end + + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceEngage( DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName ) + + self:F({DefenderSquadron=DefenderSquadron}) + self:F({DefendersNeeded=DefendersNeeded}) + self:F({Defense=Defense}) + self:F({DefenseTaskType=DefenseTaskType}) + self:F({AttackerDetection=AttackerDetection}) + self:F({SquadronName=SquadronName}) + + local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + if DefenderGroup then + + local AI_A2G = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + + local Fsm = AI_A2G[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude ) -- AI.AI_A2G_ENGAGE + Fsm:SetDispatcher( self ) + Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + Fsm:SetDisengageRadius( self.DisengageRadius ) + Fsm:Start() + + self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, Fsm, AttackerDetection, DefenderGrouping ) + + function Fsm:onafterTakeoff( Defender, From, Event, To ) + self:F({"Defender Birth", Defender:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) + + self:F( { DefenderTarget = DefenderTarget } ) + + if DefenderTarget then + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne." ) + Fsm:Engage( DefenderTarget.Set ) -- Engage on the TargetSetUnit + end + end + + function Fsm:onafterRTB( Defender, From, Event, To ) + self:F({"Defender RTB", Defender:GetName()}) + self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) + + Dispatcher:ClearDefenderTaskTarget( Defender ) + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterLostControl( Defender, From, Event, To ) + self:F({"Defender LostControl", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) + + if Defender:IsAboveRunway() then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterHome( Defender, From, Event, To, Action ) + self:F({"Defender Home", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) + + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + Dispatcher:ResourcePark( Squadron, Defender ) + end + end + end + end --- -- @param #AI_A2G_DISPATCHER self @@ -3143,83 +3733,9 @@ do -- AI_A2G_DISPATCHER end while ( DefendersNeeded > 0 ) do - - local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) - + self:ResourceQueue( false, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, ClosestDefenderSquadronName ) DefendersNeeded = DefendersNeeded - DefenderGrouping - - if DefenderGroup then - - DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead - - local AI_A2G = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } - - local Fsm = AI_A2G[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude ) -- AI.AI_A2G_ENGAGE - Fsm:SetDispatcher( self ) - Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) - Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) - Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) - Fsm:SetDisengageRadius( self.DisengageRadius ) - Fsm:Start() - - self:SetDefenderTask( ClosestDefenderSquadronName, DefenderGroup, DefenseTaskType, Fsm, AttackerDetection, DefenderGrouping ) - - function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"Defender Birth", Defender:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) - - self:F( { DefenderTarget = DefenderTarget } ) - - if DefenderTarget then - Fsm:Engage( DefenderTarget.Set ) -- Engage on the TargetSetUnit - end - end - - function Fsm:onafterRTB( Defender, From, Event, To ) - self:F({"Defender RTB", Defender:GetName()}) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) - - local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - Dispatcher:ClearDefenderTaskTarget( Defender ) - end - - --- @param #AI_A2G_DISPATCHER self - function Fsm:onafterLostControl( Defender, From, Event, To ) - self:F({"Defender LostControl", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - --if Defender:IsAboveRunway() then - --Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - --Defender:Destroy() - --end - end - - --- @param #AI_A2G_DISPATCHER self - function Fsm:onafterHome( Defender, From, Event, To, Action ) - self:F({"Defender Home", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - - local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - - if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - - if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - Dispatcher:ParkDefender( Squadron, Defender ) - end - end - end -- if DefenderGCI then + DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead end -- while ( DefendersNeeded > 0 ) do else -- No more resources, try something else. @@ -3257,6 +3773,8 @@ do -- AI_A2G_DISPATCHER self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "SEAD" ) + + if DetectedItem.IsDetected == true then @@ -3464,7 +3982,7 @@ do -- AI_A2G_DISPATCHER if self.TacticalDisplay then -- Show tactical situation - Report:Add( string.format( "\n - %4s %s ( %s ): ( #%d ) %s" , DetectedItem.Type or " --- ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) ) + Report:Add( string.format( " - %s ( %s ): ( #%d - %4s ) %s" , DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", DetectedItem.Set:GetObjectNames() ) ) for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do local Defender = Defender -- Wrapper.Group#GROUP if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then @@ -3510,6 +4028,13 @@ do -- AI_A2G_DISPATCHER end end Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) + + Report:Add( string.format( "\n - %d Queued Aircraft Launches", #self.DefenseQueue ) ) + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + local DefenseQueueItem = DefenseQueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem + Report:Add( string.format( " - %s - %s", DefenseQueueItem.SquadronName, DefenseQueueItem.DefenderSquadron.TakeoffTime, DefenseQueueItem.DefenderSquadron.TakeoffInterval) ) + + end self:F( Report:Text( "\n" ) ) trigger.action.outText( Report:Text( "\n" ), 25 ) diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index 142d73e6a..f6d1e5d93 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -159,9 +159,10 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni self:SetTargetDistance( ToCoord ) -- For RTB status check local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) - + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 25000 ) + --- Create a route point of type air, 50km from the center of the attack point. - local ToWP = ToCoord:Translate( 50000, FromEngageAngle ):WaypointAir( + local ToWP = ToCoord:Translate( EngageDistance, FromEngageAngle ):WaypointAir( self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index bbfadc5a0..8bac73d1c 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -64,6 +64,8 @@ function AI_AIR:New( AIGroup ) self:SetStartState( "Stopped" ) + self:AddTransition( "*", "Queue", "Queued" ) + self:AddTransition( "*", "Start", "Started" ) --- Start Handler OnBefore for AI_AIR @@ -400,6 +402,8 @@ function AI_AIR:SetDamageThreshold( PatrolDamageThreshold ) return self end + + --- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. -- @param #AI_AIR self -- @return #AI_AIR self @@ -537,7 +541,7 @@ function AI_AIR.RTBRoute( AIGroup, Fsm ) AIGroup:F( { "AI_AIR.RTBRoute:", AIGroup:GetName() } ) if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) + Fsm:RTB() end end @@ -573,19 +577,29 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) --- Calculate the target route point. - local CurrentCoord = AIGroup:GetCoordinate() + local FromCoord = AIGroup:GetCoordinate() local ToTargetCoord = self.HomeAirbase:GetCoordinate() local ToTargetSpeed = math.random( self.RTBMinSpeed, self.RTBMaxSpeed ) - local ToAirbaseAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) + local ToAirbaseAngle = FromCoord:GetAngleDegrees( FromCoord:GetDirectionVec3( ToTargetCoord ) ) - local Distance = CurrentCoord:Get2DDistance( ToTargetCoord ) + local Distance = FromCoord:Get2DDistance( ToTargetCoord ) - local ToAirbaseCoord = CurrentCoord:Translate( 5000, ToAirbaseAngle ) + local ToAirbaseCoord = FromCoord:Translate( 5000, ToAirbaseAngle ) if Distance < 5000 then self:E( "RTB and near the airbase!" ) self:Home() return end + + --- Create a route point of type air. + local FromRTBRoutePoint = FromCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + --- Create a route point of type air. local ToRTBRoutePoint = ToAirbaseCoord:WaypointAir( self.PatrolAltType, @@ -595,7 +609,7 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) true ) - EngageRoute[#EngageRoute+1] = ToRTBRoutePoint + EngageRoute[#EngageRoute+1] = FromRTBRoutePoint EngageRoute[#EngageRoute+1] = ToRTBRoutePoint AIGroup:OptionROEHoldFire() @@ -609,7 +623,7 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) --- NOW ROUTE THE GROUP! - AIGroup:Route( EngageRoute, 0.5 ) + AIGroup:Route( EngageRoute, 0 ) end diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index 9e4532e39..4603adf82 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -46,6 +46,7 @@ do -- DETECTION MANAGER --- @type DETECTION_MANAGER -- @field Core.Set#SET_GROUP SetGroup The groups to which the FAC will report to. -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. + -- @field Tasking.CommandCenter#COMMANDCENTER CC The command center that is used to communicate with the players. -- @extends Core.Fsm#FSM --- DETECTION_MANAGER class. @@ -218,6 +219,33 @@ do -- DETECTION MANAGER return self._ReportDisplayTime end + --- Set a command center to communicate actions to the players reporting to the command center. + -- @param #DETECTION_MANAGER self + -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. + -- @return #DETECTION_MANGER self + function DETECTION_MANAGER:SetCommandCenter( CommandCenter ) + + self.CC = CommandCenter + + return self + end + + + --- Send an information message to the players reporting to the command center. + -- @param #DETECTION_MANAGER self + -- @param #string Message The message to be sent. + -- @return #DETECTION_MANGER self + function DETECTION_MANAGER:MessageToPlayers( Message ) + + if self.CC then + self.CC:MessageToAll( Message ) + end + + return self + end + + + --- Reports the detected items to the @{Core.Set#SET_GROUP}. -- @param #DETECTION_MANAGER self -- @param Functional.Detection#DETECTION_BASE Detection From 1c063ca3088862150b1eda5084828d7e013b77eb Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 22 Jan 2019 08:51:17 +0100 Subject: [PATCH 137/147] Optimizations --- Moose Development/Moose/AI/AI_A2A_Patrol.lua | 2 +- Moose Development/Moose/AI/AI_A2G_BAI.lua | 6 +-- Moose Development/Moose/AI/AI_A2G_CAS.lua | 6 +-- Moose Development/Moose/AI/AI_A2G_Engage.lua | 8 ++-- Moose Development/Moose/AI/AI_A2G_Patrol.lua | 14 +++--- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 6 +-- Moose Development/Moose/AI/AI_Air.lua | 43 +++++++++++-------- .../Moose/Wrapper/Controllable.lua | 5 ++- 8 files changed, 50 insertions(+), 40 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Patrol.lua b/Moose Development/Moose/AI/AI_A2A_Patrol.lua index 7b44c14fb..7e57c24d0 100644 --- a/Moose Development/Moose/AI/AI_A2A_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2A_Patrol.lua @@ -345,7 +345,7 @@ function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) AIPatrol:OptionROEReturnFire() AIPatrol:OptionROTEvadeFire() - AIPatrol:Route( PatrolRoute, 0.5 ) + AIPatrol:Route( PatrolRoute, 0.5) end end diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index 5431a042e..e761e0b70 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -135,7 +135,7 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit if #AttackTasks == 0 then self:E( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() - self:__RTB( 0.5 ) + self:__RTB( self.TaskDelay ) else DefenderGroup:OptionROEOpenFire() DefenderGroup:OptionROTEvadeFire() @@ -144,11 +144,11 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) end - DefenderGroup:Route( EngageRoute, 0.5 ) + DefenderGroup:Route( EngageRoute, self.TaskDelay ) end else self:E( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() - self:__RTB( 0.5 ) + self:__RTB( self.TaskDelay ) end end diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index d3f565ce1..6ed78a5eb 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -133,7 +133,7 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit if #AttackTasks == 0 then self:E( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() - self:__RTB( 0.5 ) + self:__RTB( self.TaskDelay ) else DefenderGroup:OptionROEOpenFire() DefenderGroup:OptionROTEvadeFire() @@ -142,12 +142,12 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) end - DefenderGroup:Route( EngageRoute, 0.5 ) + DefenderGroup:Route( EngageRoute, self.TaskDelay ) end else self:E( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() - self:__RTB( 0.5 ) + self:__RTB( self.TaskDelay ) end end diff --git a/Moose Development/Moose/AI/AI_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua index e6be845c3..b71da1dce 100644 --- a/Moose Development/Moose/AI/AI_A2G_Engage.lua +++ b/Moose Development/Moose/AI/AI_A2G_Engage.lua @@ -295,10 +295,10 @@ end --- @param Wrapper.Group#GROUP AIControllable function AI_A2G_ENGAGE.EngageRoute( AIGroup, Fsm ) - AIGroup:F( { "AI_A2G_ENGAGE.EngageRoute:", AIGroup:GetName() } ) + AIGroup:I( { "AI_A2G_ENGAGE.EngageRoute:", AIGroup:GetName() } ) if AIGroup:IsAlive() then - Fsm:__Engage( 0.5 ) + Fsm:__Engage( Fsm.TaskDelay ) --local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) --AIGroup:SetTask( Task ) @@ -327,7 +327,7 @@ end function AI_A2G_ENGAGE:onafterAbort( AIGroup, From, Event, To ) AIGroup:ClearTasks() self:Return() - self:__RTB( 0.5 ) + self:__RTB( self.TaskDelay ) end @@ -372,7 +372,7 @@ function AI_A2G_ENGAGE:OnEventDead( EventData ) if EventData.IniDCSUnit then if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) + self:__Destroy( self.TaskDelay, EventData ) end end end diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua index dd9e0342a..4e8a6e953 100644 --- a/Moose Development/Moose/AI/AI_A2G_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2G_Patrol.lua @@ -234,12 +234,12 @@ function AI_A2G_PATROL:onafterPatrol( AIPatrol, From, Event, To ) self:ClearTargetDistance() - self:__Route( 1 ) + self:__Route( self.TaskDelay ) AIPatrol:OnReSpawn( function( PatrolGroup ) - self:__Reset( 1 ) - self:__Route( 5 ) + self:__Reset( self.TaskDelay ) + self:__Route( self.TaskDelay ) end ) end @@ -306,7 +306,7 @@ function AI_A2G_PATROL:onafterRoute( AIPatrol, From, Event, To ) AIPatrol:OptionROEReturnFire() AIPatrol:OptionROTEvadeFire() - AIPatrol:Route( PatrolRoute, 0.5 ) + AIPatrol:Route( PatrolRoute, self.TaskDelay ) end end @@ -314,10 +314,10 @@ end --- @param Wrapper.Group#GROUP AIPatrol function AI_A2G_PATROL.Resume( AIPatrol, Fsm ) - AIPatrol:I( { "AI_A2G_PATROL.Resume:", AIPatrol:GetName() } ) + AIPatrol:F( { "AI_A2G_PATROL.Resume:", AIPatrol:GetName() } ) if AIPatrol:IsAlive() then - Fsm:__Reset( 1 ) - Fsm:__Route( 5 ) + Fsm:__Reset( self.TaskDelay ) + Fsm:__Route( self.TaskDelay ) end end diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index f6d1e5d93..72d22577c 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -191,7 +191,7 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni if #AttackTasks == 0 then self:E( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() - self:__RTB( 0.5 ) + self:__RTB( self.TaskDelay ) else DefenderGroup:OptionROEOpenFire() DefenderGroup:OptionROTVertical() @@ -202,7 +202,7 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) end - DefenderGroup:Route( EngageRoute, 2 ) + DefenderGroup:Route( EngageRoute, self.TaskDelay ) -- else -- local AttackTasks = {} @@ -230,7 +230,7 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni else self:E( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() - self:__RTB( 0.5 ) + self:__RTB( self.TaskDelay ) end end diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 8bac73d1c..149834022 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -51,6 +51,8 @@ AI_AIR = { ClassName = "AI_AIR", } +AI_AIR.TaskDelay = 0.5 -- The delay of each task given to the AI. + --- Creates a new AI_AIR process. -- @param #AI_AIR self -- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process. @@ -524,7 +526,7 @@ function AI_AIR:onafterStatus() end if RTB == true then - self:__RTB( 0.5 ) + self:__RTB( self.TaskDelay ) end if not self:Is("Home") then @@ -551,7 +553,7 @@ function AI_AIR.RTBHold( AIGroup, Fsm ) AIGroup:F( { "AI_AIR.RTBHold:", AIGroup:GetName() } ) if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) + Fsm:__RTB( Fsm.TaskDelay ) Fsm:Return() local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) AIGroup:SetTask( Task ) @@ -612,18 +614,16 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) EngageRoute[#EngageRoute+1] = FromRTBRoutePoint EngageRoute[#EngageRoute+1] = ToRTBRoutePoint + local Tasks = {} + Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_A2G_ENGAGE.RTBRoute", self ) + + EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) + AIGroup:OptionROEHoldFire() AIGroup:OptionROTEvadeFire() - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - AIGroup:WayPointInitialize( EngageRoute ) - - local Tasks = {} - Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_AIR.RTBRoute", self ) - EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) - --- NOW ROUTE THE GROUP! - AIGroup:Route( EngageRoute, 0 ) + AIGroup:Route( EngageRoute, self.TaskDelay ) end @@ -670,7 +670,7 @@ function AI_AIR.Resume( AIGroup, Fsm ) AIGroup:I( { "AI_AIR.Resume:", AIGroup:GetName() } ) if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) + Fsm:__RTB( Fsm.TaskDelay ) end end @@ -690,10 +690,19 @@ function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) --- Calculate the target route point. - local CurrentCoord = AIGroup:GetCoordinate() + local FromRefuelCoord = AIGroup:GetCoordinate() local ToRefuelCoord = Tanker:GetCoordinate() local ToRefuelSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + --- Create a route point of type air. + local FromRefuelRoutePoint = FromRefuelCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToRefuelSpeed, + true + ) + --- Create a route point of type air. local ToRefuelRoutePoint = ToRefuelCoord:WaypointAir( self.PatrolAltType, @@ -705,7 +714,7 @@ function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) self:F( { ToRefuelSpeed = ToRefuelSpeed } ) - RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint + RefuelRoute[#RefuelRoute+1] = FromRefuelRoutePoint RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint AIGroup:OptionROEHoldFire() @@ -716,7 +725,7 @@ function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) Tasks[#Tasks+1] = AIGroup:TaskFunction( self:GetClassName() .. ".Resume", self ) RefuelRoute[#RefuelRoute].task = AIGroup:TaskCombo( Tasks ) - AIGroup:Route( RefuelRoute, 0.5 ) + AIGroup:Route( RefuelRoute, self.TaskDelay ) else self:RTB() end @@ -739,7 +748,7 @@ function AI_AIR:OnCrash( EventData ) if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then self:E( self.Controllable:GetUnits() ) if #self.Controllable:GetUnits() == 1 then - self:__Crash( 1, EventData ) + self:__Crash( self.TaskDelay, EventData ) end end end @@ -749,7 +758,7 @@ end function AI_AIR:OnEjection( EventData ) if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__Eject( 1, EventData ) + self:__Eject( self.TaskDelay, EventData ) end end @@ -758,6 +767,6 @@ end function AI_AIR:OnPilotDead( EventData ) if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__PilotDead( 1, EventData ) + self:__PilotDead( self.TaskDelay, EventData ) end end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 0353c42f9..96455d007 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -400,7 +400,7 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) local Controller = self:_GetController() --self:I( "Before SetTask" ) Controller:setTask( DCSTask ) - --self:I( "After SetTask" ) + self:F( { DCSTask = DCSTask } ) else BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) end @@ -408,6 +408,7 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) if not WaitTime or WaitTime == 0 then SetTask( self, DCSTask ) + self:F( { DCSTask = DCSTask } ) else self.TaskScheduler:Schedule( self, SetTask, { DCSTask }, WaitTime ) end @@ -1036,7 +1037,7 @@ end -- @param #CONTROLLABLE self -- @param #number Altitude The altitude [m] to hold the position. -- @param #number Speed The speed [m/s] flying when holding the position. --- @param Core.Point#COORDINATE Coordinate The coordinate where to orbit. +-- @param Core.Point#COORDINATE Coordinate (optional) The coordinate where to orbit. If the coordinate is not given, then the current position of the controllable is used. -- @return #CONTROLLABLE self function CONTROLLABLE:TaskOrbitCircle( Altitude, Speed, Coordinate ) self:F2( { self.ControllableName, Altitude, Speed } ) From 265d6da3313e52a5cdf8c6bbf329820261096303 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 23 Jan 2019 11:00:36 +0100 Subject: [PATCH 138/147] AIRBOSS v0.9.1 --- Moose Development/Moose/Core/Radio.lua | 6 +- .../Moose/Functional/Warehouse.lua | 34 +- Moose Development/Moose/Ops/Airboss.lua | 986 +++++++++++------- .../Moose/Ops/RecoveryTanker.lua | 20 +- Moose Development/Moose/Ops/RescueHelo.lua | 13 +- 5 files changed, 693 insertions(+), 366 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 662b7b94f..6155aa036 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -461,13 +461,13 @@ BEACON.Type={ ICLS = 131584, } ---- Beacon systems supported by DCS. +--- Beacon systems supported by DCS. https://wiki.hoggitworld.com/view/DCS_command_activateBeacon -- @type BEACON.System -- @field #number PAR_10 -- @field #number RSBN_5 -- @field #number TACAN -- @field #number TACAN_TANKER --- @field #number ILS_LOCALIZER +-- @field #number ILS_LOCALIZER (This is the one to be used for AA TACAN Tanker!) -- @field #number ILS_GLIDESLOPE -- @field #number BROADCAST_STATION BEACON.System={ @@ -539,7 +539,7 @@ function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) -- Check if unit is an aircraft and set system accordingly. local AA=self.Positionable:IsAir() if AA then - System=BEACON.System.TACAN_TANKER + System=5 --NOTE: 5 is how you cat the correct tanker behaviour! --BEACON.System.TACAN_TANKER -- Check if "Y" mode is selected for aircraft. if Mode~="Y" then self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable}) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index ed4b8c31f..c2b21cdd7 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -4,9 +4,9 @@ -- -- ## Features: -- --- * Holds (virtual) assests in stock and spawns them upon request. +-- * Holds (virtual) assets in stock and spawns them upon request. -- * Manages requests of assets from other warehouses. --- * Queueing system with optional priorization of requests. +-- * Queueing system with optional prioritization of requests. -- * Realistic transportation of assets between warehouses. -- * Different means of automatic transportation (planes, helicopters, APCs, self propelled). -- * Strategic components such as capturing, defending and destroying warehouses and their associated infrastructure. @@ -68,7 +68,7 @@ -- @field #number spawnzonemaxdist Max distance between warehouse and spawn zone. Default 5000 meters. -- @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 #string autosavefile 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. -- @field #boolean isunit If true, warehouse is represented by a unit instead of a static. -- @extends Core.Fsm#FSM @@ -84,7 +84,7 @@ -- this means they can be destroyed during the transport and add more life to the DCS world. -- -- This comes along with some additional interesting stategic aspects since capturing/defending and destroying/protecting an enemy or your --- own warehous becomes of critical importance for the development of a conflict. +-- own warehouse becomes of critical importance for the development of a conflict. -- -- In essence, creating an efficient network of warehouses is vital for the success of a battle or even the whole war. Likewise, of course, cutting off the enemy -- of important supply lines by capturing or destroying warehouses or their associated infrastructure is equally important. @@ -1733,7 +1733,7 @@ WAREHOUSE.db = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.7" +WAREHOUSE.version="0.6.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1802,10 +1802,11 @@ function WAREHOUSE:New(warehouse, alias) -- Check if just a string was given and convert to static. if type(warehouse)=="string" then - warehouse=UNIT:FindByName(warehouse) + local warehousename=warehouse + warehouse=UNIT:FindByName(warehousename) if warehouse==nil then - env.info(string.format("No warehouse unit with name %s found trying static.", warehouse)) - warehouse=STATIC:FindByName(warehouse, true) + env.info(string.format("No warehouse unit with name %s found trying static.", tostring(warehousename))) + warehouse=STATIC:FindByName(warehousename, true) self.isunit=false else self.isunit=true @@ -4795,9 +4796,8 @@ function WAREHOUSE:onafterCaptured(From, Event, To, Coalition, Country) local text=string.format("Warehouse %s: We were captured by enemy coalition (side=%d)!", self.alias, Coalition) self:_InfoMessage(text) - -- Change coalition and country of warehouse static. - self:ChangeCoaliton(Coalition, Country) - + -- Warehouse respawned. + self:ChangeCountry(Country) end @@ -5173,9 +5173,12 @@ function WAREHOUSE:_SpawnAssetRequest(Request) -- Spawn train. if self.rail then --TODO: Rail should only get one asset because they would spawn on top! + + -- Spawn naval assets. + _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.spawnzone) end - self:E(self.wid.."ERROR: Spawning of TRAIN assets not possible yet!") + --self:E(self.wid.."ERROR: Spawning of TRAIN assets not possible yet!") elseif _assetitem.category==Group.Category.SHIP then @@ -5220,7 +5223,7 @@ end -- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aioff) - if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP) then + if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP or asset.category==Group.Category.TRAIN) then -- Prepare spawn template. local template=self:_SpawnAssetPrepareTemplate(asset, alias) @@ -5230,6 +5233,11 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aiof -- Get a random coordinate in the spawn zone. local coord=spawnzone:GetRandomCoordinate() + + -- For trains, we use the rail connection point. + if asset.category==Group.Category.TRAIN then + coord=self.rail + end -- Translate the position of the units. for i=1,#template.units do diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index ea4000e4b..df2091a06 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -6,7 +6,7 @@ -- -- * CASE I, II and III recoveries. -- * Supports human pilots as well as AI flight groups. --- * Automatic LSO grading (WIP) including (optional) live grading while in the groove. +-- * Automatic LSO grading including (optional) live grading while in the groove. -- * Different skill levels from on-the-fly tips for flight students to *ziplip* for pros. Can be set for each player individually. -- * Define recovery time windows with individual recovery cases in the same mission. -- * Automatic TACAN and ICLS channel setting of carrier. @@ -15,6 +15,7 @@ -- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, help function (aircraft attitude, marking of zones etc). -- * Recovery tanker and refueling option via integration of @{Ops.RecoveryTanker} class. -- * Rescue helicopter option via @{Ops.RescueHelo} class. +-- * Combine multiple human player to sections. -- * Many parameters customizable by convenient user API functions. -- * Multiple carrier support due to object oriented approach. -- * Unlimited number of players. @@ -84,10 +85,10 @@ -- @field #boolean ICLSon Automatic ICLS is activated. -- @field #number ICLSchannel ICLS channel. -- @field #string ICLSmorse ICLS morse code, e.g. "STN". --- @field Core.Radio#RADIO LSORadio Radio for LSO calls. +-- @field #AIRBOSS.Radio LSORadio Radio for LSO calls. -- @field #number LSOFreq LSO radio frequency in MHz. -- @field #string LSOModu LSO radio modulation "AM" or "FM". --- @field Core.Radio#RADIO MarshalRadio Radio for carrier calls. +-- @field #AIRBOSS.Radio MarshalRadio Radio for carrier calls. -- @field #number MarshalFreq Marshal radio frequency in MHz. -- @field #string MarshalModu Marshal radio modulation "AM" or "FM". -- @field Core.Scheduler#SCHEDULER radiotimer Radio queue scheduler. @@ -147,6 +148,7 @@ -- @field #boolean staticweather Mission uses static rather than dynamic weather. -- @field #number windowcount Running number counting the recovery windows. -- @field #number LSOdT Time interval in seconds before the LSO will make its next call. +-- @field #string senderac Name of the aircraft acting as sender for broadcasting radio messages from the carrier. DCS shortcoming workaround. -- @extends Core.Fsm#FSM --- Be the boss! @@ -323,6 +325,11 @@ -- -- Marshal will transmit a short message on his radio frequency. See @{#AIRBOSS.SetMarshalRadio}. -- +-- ### Subtitles On/Off +-- +-- This command toggles the display of radio message subtitles. By default subtitles are on. +-- Note that subtitles for radio messages which do not have a complete voice over are always displayed. +-- -- ## Kneeboard Menu -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuKneeboard.png) @@ -361,9 +368,9 @@ -- -- The responsibilities of the section leader are: -- --- * To request Marshal. The section members are not allowed to do this and have to follow the lead to his assigned stack. --- * To lead the right way to the pattern if the flight is allowed to commence. --- * The lead is also the only one who can request commence if the flight wants to bypass the Marshal stack. +-- * To request Marshal. The section members are not allowed to do this and have to follow the lead to his assigned stack. +-- * To lead the right way to the pattern if the flight is allowed to commence. +-- * The lead is also the only one who can request commence if the flight wants to bypass the Marshal stack. -- -- Each time the command is issued by the lead, the complete section is set up from scratch. Members which are not inside the 100 m radius any more are -- removed and/or new members which are now in range are added. @@ -530,7 +537,7 @@ -- Players can request marshal via the F10 menu and will also be given a marshal stack. Currently, human players can request commence via the F10 radio regardless of -- whether a window is open or not and will be allowed to enter the pattern (if not already full). This will probably change in the future. -- --- At the moment there is no automatic recovery case set depending on weather or daytime. So it is the AIRBOSS (you) who needs to make that decision. +-- At the moment there is no automatic recovery case set depending on weather or daytime. So it is the AIRBOSS (i.e. you as mission designer) who needs to make that decision. -- It is probably a good idea to synchronize the timing with the waypoints of the carrier. For example, setting up the waypoints such that the carrier -- already has turning into the wind, when a recovery window opens. -- @@ -679,6 +686,34 @@ -- -- === -- +-- # Examples +-- +-- In this section a few simple examples are given to illustrate the scripting part. +-- +-- ## Simple Case +-- +-- -- Create AIRBOSS object. +-- local AirbossStennis=AIRBOSS:New("USS Stennis") +-- +-- -- Add recovery windows: +-- -- Case I from 9 to 12 am. +-- local window1=AirbossStennis:AddRecoveryWindow("8:55", "12:00", 1) +-- -- Case II with +15 degrees holding offset from 1500 for 90 min. +-- local window2=AirbossStennis:AddRecoveryWindow("14:55", "16:30", 2, 15) +-- -- Case III with +30 degrees holding offset from 2100 to 2330. +-- local window3=AirbossStennis:AddRecoveryWindow("21:00", "23:30", 3, 30) +-- +-- -- Load all saved player grades from your "Saved Games\DCS" folder (if lfs was desanitized). +-- AirbossStennis:Load() +-- +-- -- Automatically save player results to your "Saved Games\DCS" folder each time a player get a final grade from the LSO. +-- AirbossStennis:SetAutoSave() +-- +-- -- Start airboss class. +-- AirbossStennis:Start() +-- +-- === +-- -- # Debugging -- -- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in @@ -782,6 +817,7 @@ AIRBOSS = { staticweather = nil, windowcount = 0, LSOdT = nil, + senderac = nil, } --- Player aircraft types capable of landing on carriers. @@ -932,6 +968,12 @@ AIRBOSS.GroovePos={ IW="IW", } +--- Radio. +-- @type AIRBOSS.Radio +-- @field #number frequency Frequency in Hz. +-- @field #number modulation Band modulation. +-- @field #string alias Radio alias. + --- Radio sound file and subtitle. -- @type AIRBOSS.RadioCall -- @field #string file Sound file name without suffix. @@ -939,6 +981,10 @@ AIRBOSS.GroovePos={ -- @field #boolean loud Loud version of sound file available. -- @field #string subtitle Subtitle displayed during transmission. -- @field #number duration Duration of the sound in seconds. This is also the duration the subtitle is displayed. +-- @field #number subduration Duration in seconds the subtitle is displayed. +-- @field #string modexsender Onboard number of the sender (optional). +-- @field #string modexreceiver Onboard number of the receiver (optional). +-- @field #string sender Sender of the message (optional). Default radia alias. --- LSO radio calls. -- @type AIRBOSS.LSOCall @@ -969,6 +1015,8 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall N7 "Seven" call. -- @field #AIRBOSS.RadioCall N8 "Eight" call. -- @field #AIRBOSS.RadioCall N9 "Nine" call. +-- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. +-- @field #AIRBOSS.RadioCall NOISE Static noise sound. AIRBOSS.LSOCall={ RADIOCHECK={ file="LSO-RadioCheck", @@ -976,6 +1024,7 @@ AIRBOSS.LSOCall={ loud=false, subtitle="Paddles, radio check", duration=1.1, + subduration=5, }, RIGHTFORLINEUP={ file="LSO-RightForLineup", @@ -983,6 +1032,7 @@ AIRBOSS.LSOCall={ loud=true, subtitle="Right for line up", duration=0.80, + subduration=1, }, COMELEFT={ file="LSO-ComeLeft", @@ -990,6 +1040,7 @@ AIRBOSS.LSOCall={ loud=true, subtitle="Come left", duration=0.60, + subduration=1, }, HIGH={ file="LSO-High", @@ -997,6 +1048,7 @@ AIRBOSS.LSOCall={ loud=true, subtitle="You're high", duration=0.65, + subduration=1, }, LOW={ file="LSO-Low", @@ -1004,6 +1056,7 @@ AIRBOSS.LSOCall={ loud=true, subtitle="You're low", duration=0.50, + subduration=1, }, POWER={ file="LSO-Power", @@ -1011,6 +1064,7 @@ AIRBOSS.LSOCall={ loud=true, subtitle="Power", duration=0.50, --0.45 was too short + subduration=1, }, SLOW={ file="LSO-Slow", @@ -1018,6 +1072,7 @@ AIRBOSS.LSOCall={ loud=true, subtitle="You're slow", duration=0.65, + subduration=1, }, FAST={ file="LSO-Fast", @@ -1025,6 +1080,7 @@ AIRBOSS.LSOCall={ loud=true, subtitle="You're fast", duration=0.7, + subduration=1, }, CALLTHEBALL={ file="LSO-CallTheBall", @@ -1032,6 +1088,7 @@ AIRBOSS.LSOCall={ loud=false, subtitle="Call the ball", duration=0.6, + subduration=2, }, ROGERBALL={ file="LSO-RogerBall", @@ -1039,6 +1096,7 @@ AIRBOSS.LSOCall={ loud=false, subtitle="Roger ball", duration=0.7, + subduration=2, }, WAVEOFF={ file="LSO-WaveOff", @@ -1046,6 +1104,7 @@ AIRBOSS.LSOCall={ loud=false, subtitle="Wave off", duration=0.6, + subduration=5, }, BOLTER={ file="LSO-BolterBolter", @@ -1053,6 +1112,7 @@ AIRBOSS.LSOCall={ loud=false, subtitle="Bolter, Bolter", duration=0.75, + subduration=5, }, LONGINGROOVE={ file="LSO-LongInTheGroove", @@ -1060,6 +1120,7 @@ AIRBOSS.LSOCall={ loud=false, subtitle="You're long in the groove", duration=1.2, + subduration=5, }, FOULDECK={ file="LSO-FoulDeck", @@ -1067,6 +1128,7 @@ AIRBOSS.LSOCall={ loud=false, subtitle="Foul deck", duration=0.62, + subduration=5, }, DEPARTANDREENTER={ file="LSO-DepartAndReenter", @@ -1074,6 +1136,7 @@ AIRBOSS.LSOCall={ loud=false, subtitle="Depart and re-enter", duration=1.1, + subduration=5, }, PADDLESCONTACT={ file="LSO-PaddlesContact", @@ -1081,13 +1144,15 @@ AIRBOSS.LSOCall={ loud=false, subtitle="Paddles, contact", duration=1.0, + subduration=5, }, WELCOMEABOARD={ file="LSO-WelcomeAboard", suffix="ogg", loud=false, subtitle="Welcome aboard", - duration=0.9, + duration=1.0, + subduration=5, }, N0={ file="LSO-N0", @@ -1157,7 +1222,21 @@ AIRBOSS.LSOCall={ suffix="ogg", loud=false, subtitle="", - duration=0.40, --0.38 too short + duration=0.40, + }, + CLICK={ + file="AIRBOSS-RadioClick", + suffix="ogg", + loud=false, + subtitle="", + duration=0.35, + }, + NOISE={ + file="AIRBOSS-Noise", + suffix="ogg", + loud=false, + subtitle="", + duration=3.6, }, } @@ -1176,13 +1255,16 @@ AIRBOSS.LSOCall={ -- @field #AIRBOSS.RadioCall N7 "Seven" call. -- @field #AIRBOSS.RadioCall N8 "Eight" call. -- @field #AIRBOSS.RadioCall N9 "Nine" call. +-- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. +-- @field #AIRBOSS.RadioCall NOISE Static noise sound. AIRBOSS.MarshalCall={ RADIOCHECK={ file="MARSHAL-RadioCheck", suffix="ogg", loud=false, subtitle="Radio check", - duration=1.0, + duration=1.1, + subduration=5, }, SAYNEEDLES={ file="MARSHAL-SayNeedles", @@ -1190,6 +1272,7 @@ AIRBOSS.MarshalCall={ loud=false, subtitle="Say needles", duration=0.9, + subduration=5, }, FLYNEEDLES={ file="MARSHAL-FlyYourNeedles", @@ -1197,6 +1280,7 @@ AIRBOSS.MarshalCall={ loud=false, subtitle="Fly your needles", duration=0.9, + subduration=5, }, -- TODO: Other voice overs for marshal. N0={ @@ -1269,6 +1353,20 @@ AIRBOSS.MarshalCall={ subtitle="", duration=0.40, --0.38 too short }, + CLICK={ + file="AIRBOSS-RadioClick", + suffix="ogg", + loud=false, + subtitle="", + duration=0.35, + }, + NOISE={ + file="AIRBOSS-Noise", + suffix="ogg", + loud=false, + subtitle="", + duration=3.6, + }, } --- Difficulty level. @@ -1389,7 +1487,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.0" +AIRBOSS.version="0.9.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1502,13 +1600,9 @@ function AIRBOSS:New(carriername, alias) ------------- -- Set up Airboss radio. - self.MarshalRadio=RADIO:New(self.carrier) - self.MarshalRadio:SetAlias("MARSHAL") - self:SetMarshalRadio() + self:SetMarshalRadio() -- Set up LSO radio. - self.LSORadio=RADIO:New(self.carrier) - self.LSORadio:SetAlias("LSO") self:SetLSORadio() -- Set LSO call interval. Default 4 sec. @@ -1533,10 +1627,10 @@ function AIRBOSS:New(carriername, alias) self:SetMaxMarshalStacks() -- Set max section members. Default 2. - self:SetMaxSectionSize(2) + self:SetMaxSectionSize() -- Set max flights per stack. Default is 2. - self:SetMaxFlightsPerStack(2) + self:SetMaxFlightsPerStack() -- Set AI handling On. self:SetHandleAION() @@ -2226,8 +2320,8 @@ end -- @return #AIRBOSS self function AIRBOSS:SetLSORadio(frequency, modulation) - self.LSOFreq=frequency or 264 - self.LSOModu=modulation or "AM" + self.LSOFreq=(frequency or 264) + modulation=modulation or "AM" if modulation=="FM" then self.LSOModu=radio.modulation.FM @@ -2235,9 +2329,11 @@ function AIRBOSS:SetLSORadio(frequency, modulation) self.LSOModu=radio.modulation.AM end - self.LSORadio:SetFrequency(self.LSOFreq) - self.LSORadio:SetModulation(self.LSOModu) - + self.LSORadio={} --#AIRBOSS.Radio + self.LSORadio.frequency=self.LSOFreq + self.LSORadio.modulation=self.LSOModu + self.LSORadio.alias="LSO" + return self end @@ -2249,7 +2345,7 @@ end function AIRBOSS:SetMarshalRadio(frequency, modulation) self.MarshalFreq=frequency or 305 - self.MarshalModu=modulation or "AM" + modulation=modulation or "AM" if modulation=="FM" then self.MarshalModu=radio.modulation.FM @@ -2257,9 +2353,20 @@ function AIRBOSS:SetMarshalRadio(frequency, modulation) self.MarshalModu=radio.modulation.AM end - self.MarshalRadio:SetFrequency(self.MarshalFreq) - self.MarshalRadio:SetModulation(self.MarshalModu) + self.MarshalRadio={} --#AIRBOSS.Radio + self.MarshalRadio.frequency=self.MarshalFreq + self.MarshalRadio.modulation=self.MarshalModu + self.MarshalRadio.alias="MARSHAL" + + return self +end +--- Set unit name for sending radio messages. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #AIRBOSS self +function AIRBOSS:SetRadioUnitName(unitname) + self.senderac=unitname return self end @@ -2611,7 +2718,7 @@ function AIRBOSS:_CheckAIStatus() -- Pilot: "405, Hornet Ball, 3.2" -- TODO: Voice over. local text=string.format("%s Ball, %.1f.", self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) - self:MessageToPattern(text, element.onboard, "", 3) + self:MessageToPattern(text, element.onboard, "", 5) -- Debug message. MESSAGE:New(string.format("%s, %s", element.onboard, text), 15, "DEBUG"):ToAllIf(self.Debug) @@ -4026,7 +4133,7 @@ function AIRBOSS:_WaitPlayer(playerData) end -- Send message. - self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 10) + self:MessageToMarshal(text, "AIRBOSS", playerData.onboard, 10) -- Add player flight to waiting queue. table.insert(self.Qwaiting, playerData) @@ -4044,7 +4151,6 @@ function AIRBOSS:_WaitPlayer(playerData) flight.step=AIRBOSS.PatternStep.WAITING flight.time=timer.getAbsTime() flight.warning=nil - self:MessageToPlayer(flight, text, "AIRBOSS", nil, 10) end end @@ -4700,33 +4806,6 @@ function AIRBOSS:_GetFreeStack(ai, case, empty) -- Recovery case. case=case or self.case - --[[ - - -- Get stack - local nfull - if case==1 then - -- Lowest Case I stack. - nfull=self:_GetQueueInfo(self.Qmarshal, 1) - else - -- Lowest Case II or III stack. - nfull=self:_GetQueueInfo(self.Qmarshal, 23) - end - - -- Simple case without a recovery tanker for now. - local nfree=nfull+1 - - -- Case I stacks are limited to angels 2, 3, 4, 5 ==> so 4 free stacks. At Angels 6, we might have a tanker. - -- Next flights are asked to hold outside 10 NM zone. - if case==1 and nfree>self.Nmaxmarshal then - nfree=nil - end - - return nfree - - ]] - - -- New version. - -- Max number of stacks available. local nmaxstacks=100 if case==1 then @@ -4765,7 +4844,7 @@ function AIRBOSS:_GetFreeStack(ai, case, empty) -- Loop over stacks and check which one has a place left. local nfree=nil for i=1,nmaxstacks do - env.info(string.format("FF stack[%d]=%d", i, stack[i])) + self:T2(self.lid..string.format("FF Stack[%d]=%d", i, stack[i])) if ai or empty then -- AI need the whole stack. if stack[i]==self.NmaxStack then @@ -5130,8 +5209,9 @@ function AIRBOSS:_InitPlayer(playerData, step) -- Set us up on final if group name contains "Groove". But only for the first pass. if playerData.group:GetName():match("Groove") and playerData.passes==0 then self:MessageToPlayer(playerData, "Group name contains \"Groove\". Happy groove testing.") - playerData.attitudemonitor=false + playerData.attitudemonitor=false playerData.step=AIRBOSS.PatternStep.FINAL + table.insert(self.Qpattern, playerData) end return playerData @@ -5716,7 +5796,7 @@ function AIRBOSS:_CheckMissedStepOnEntry(playerData) local rightqueue=self:_InQueue(self.Qpattern, playerData.group) local rightflag=playerData.flag:Get()~=-42 - -- Steps that the player could of missed during Case II/III. + -- Steps that the player could have missed during Case II/III. local step=playerData.step local missedstep=step==AIRBOSS.PatternStep.PLATFORM or step==AIRBOSS.PatternStep.ARCIN or step==AIRBOSS.PatternStep.ARCOUT or step==AIRBOSS.PatternStep.DIRTYUP @@ -5807,13 +5887,6 @@ function AIRBOSS:OnEventBirth(EventData) local text=string.format("Player entered aircraft of other coalition.") MESSAGE:New(text, 30):ToAllIf(self.Debug) self:T(self.lid..text) - --[[ - local gid=_group:GetID() - if self.menuadded[gid] then - if AIRBOSS.MenuRoot[gid] then - end - end - ]] return end @@ -6222,8 +6295,8 @@ function AIRBOSS:_Holding(playerData) -- TODO: check if this works. --local dT=timer.getAbsTime()-playerData.time - -- Check if less then 60 seconds. - if dT<=60 then + -- Check if less then 90 seconds. + if dT<=90 then justcollapsed=true end end @@ -6372,7 +6445,7 @@ function AIRBOSS:_Commencing(playerData, zonecheck) self:_InitPlayer(playerData) -- Commencing message to player only. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then -- Text local text="" @@ -6666,10 +6739,11 @@ function AIRBOSS:_DirtyUp(playerData) end -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. - self:_Number2Radio(self.MarshalRadio, playerData.onboard, 20) - self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.SAYNEEDLES, false, 20.1) - self:_Number2Radio(self.MarshalRadio, playerData.onboard, 25) - self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.FLYNEEDLES, false, 25.1) + local callsay=self:_NewRadioCall(AIRBOSS.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard) + local callfly=self:_NewRadioCall(AIRBOSS.MarshalCall.FLYNEEDLES, nil, nil, 5, playerData.onboard) + self:RadioTransmission(self.MarshalRadio, callsay, false, 40) + self:RadioTransmission(self.MarshalRadio, callfly, false, 45) + -- TODO: Make Fly Bullseye call if no automatic ICLS is active. -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). @@ -7099,8 +7173,7 @@ function AIRBOSS:_Final(playerData) playerData.groove.X0=groovedata -- Next step: X start. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) - + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) end end @@ -7473,7 +7546,9 @@ function AIRBOSS:_CheckFoulDeck(playerData) local text=string.format("Unit %s on landing runway ==> Foul deck!", unit:GetName()) self:T(self.lid..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) - --runway:FlareZone(FLARECOLOR.Red, 30) + if self.Debug then + runway:FlareZone(FLARECOLOR.Red, 30) + end fouldeck=true foulunit=unit end @@ -7493,7 +7568,7 @@ function AIRBOSS:_CheckFoulDeck(playerData) self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WAVEOFF, false, 1.2) -- Player hint for flight students. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then local text=string.format("overfly landing area and enter bolter pattern.") self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 3) end @@ -7508,7 +7583,7 @@ function AIRBOSS:_CheckFoulDeck(playerData) if foulunit then local foulflight=self:_GetFlightFromGroupInQueue(foulunit:GetGroup(), self.flights) if foulflight and not foulflight.ai then - self:MessageToPlayer(foulflight, "Move your ass from my runway. NOW!", "AIRBOSS", nil, 10) + self:MessageToPlayer(foulflight, "move your ass from my runway. NOW!", "AIRBOSS", nil, 10) end end end @@ -9185,31 +9260,25 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) -- Altitude error +-X% local _error=(altitude-altopt)/altopt*100 - -- TODO: radio call for flight students. - --local radiocall={} --#AIRBOSS.RadioCall + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall - local hint + local hint="" + local loud=false if _error>badscore then - hint=string.format("You're high.") - -- TODO: Dang! This overwrites the array! - --radiocall=AIRBOSS.LSOCall.HIGH - --radiocall.loud=true - --radiocall.subtitle="" + --hint=string.format("You're high.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.HIGH, nil, nil, 5) + loud=true elseif _error>lowscore then - hint= string.format("You're slightly high.") - --radiocall=AIRBOSS.LSOCall.HIGH - --radiocall.loud=false - --radiocall.subtitle="" + --hint= string.format("You're slightly high.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.HIGH, nil, nil, 5) elseif _error<-badscore then - hint=string.format("You're low. ") - --radiocall=AIRBOSS.LSOCall.LOW - --radiocall.loud=true - --radiocall.subtitle="" + --hint=string.format("You're low. ") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.LOW, nil, nil, 5) + loud=true elseif _error<-lowscore then - hint=string.format("You're slightly low.") - --radiocall=AIRBOSS.LSOCall.LOW - --radiocall.loud=false - --radiocall.subtitle="" + --hint=string.format("You're slightly low.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.LOW, nil, nil, 5) else hint=string.format("Good altitude.") end @@ -9218,8 +9287,11 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) if playerData.difficulty==AIRBOSS.Difficulty.EASY then -- Also inform students about the optimal altitude. hint=hint..string.format(" Optimal altitude is %d ft.", UTILS.MetersToFeet(altopt)) + self:Sound2Player(playerData, self.LSORadio, radiocall, loud) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then -- We keep it short normally. + hint="" + self:Sound2Player(playerData, self.LSORadio, radiocall, loud) elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then -- No hint at all for the pros. hint="" @@ -9305,29 +9377,43 @@ function AIRBOSS:_AoACheck(playerData, optaoa) -- Get aircraft AoA parameters. local aircraftaoa=self:_GetAircraftAoA(playerData) + + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall -- Rate aoa. local hint="" + local loud=false if aoa>=aircraftaoa.SLOW then - hint="Your're slow!" + --hint="Your're slow!" + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.SLOW, nil, nil, 5) + loud=true elseif aoa>=aircraftaoa.Slow then - hint="Your're slow." + --hint="Your're slow." + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.SLOW, nil, nil, 5) elseif aoa>=aircraftaoa.OnSpeedMax then hint="Your're a little slow." elseif aoa>=aircraftaoa.OnSpeedMin then hint="You're on speed." elseif aoa>=aircraftaoa.Fast then hint="You're a little fast." + elseif aoa>=aircraftaoa.FAST then + --hint="Your're fast." + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.FAST, nil, nil, 5) else - hint="You're fast!" + --hint="You're fast!" + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.FAST, nil, nil, 5) + loud=true end -- Extend or decrease depending on skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then -- Also inform students about optimal value. hint=hint..string.format(" Optimal AoA is %.1f.", self:_AoADeg2Units(playerData, optaoa)) + self:Sound2Player(playerData, self.LSORadio, radiocall, loud) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then -- We keep is short normally. + self:Sound2Player(playerData, self.LSORadio, radiocall, loud) elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then -- No hint at all for the pros. hint="" @@ -9360,15 +9446,25 @@ function AIRBOSS:_SpeedCheck(playerData, speedopt) -- Altitude error +-X% local _error=(speed-speedopt)/speedopt*100 - local hint + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall + + local hint="" + local loud=false if _error>badscore then - hint=string.format("You're fast.") + --hint=string.format("You're fast.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.FAST, "AIRBOSS", nil, 5) + loud=true elseif _error>lowscore then - hint= string.format("You're slightly fast.") + --hint= string.format("You're slightly fast.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.FAST, "AIRBOSS", nil, 5) elseif _error<-badscore then - hint=string.format("You're low.") + --hint=string.format("You're slow.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.SLOW, "AIRBOSS", nil, 5) elseif _error<-lowscore then - hint=string.format("You're slightly slow.") + --hint=string.format("You're slightly slow.") + radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.SLOW, "AIRBOSS", nil, 5) + loud=true else hint=string.format("Good speed.") end @@ -9376,8 +9472,10 @@ function AIRBOSS:_SpeedCheck(playerData, speedopt) -- Extend or decrease depending on skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then hint=hint..string.format(" Optimal speed is %d knots.", UTILS.MpsToKnots(speedopt)) + self:Sound2Player(playerData, self.LSORadio, radiocall, loud) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then -- We keep is short normally. + self:Sound2Player(playerData, self.LSORadio, radiocall, loud) elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then -- No hint at all for pros. hint="" @@ -9551,8 +9649,22 @@ function AIRBOSS:_Debrief(playerData) -- Foul Deck -- --------------- - -- Bolter pattern. Then Abeam or bullseye. - playerData.step=AIRBOSS.PatternStep.BOLTER + if playerData.unit:InAir() then + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + + else + + -- Welcome aboard! + self:Sound2Player(playerData, self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) + + -- Airboss talkto! + local text=string.format("the deck was fouled but landed anyway. Airboss wants to talk to you!") + self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 3) + + end + elseif playerData.waveoff then @@ -9568,7 +9680,7 @@ function AIRBOSS:_Debrief(playerData) else -- Welcome aboard! - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) + self:Sound2Player(playerData, self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) -- Airboss talkto! local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") @@ -9598,7 +9710,7 @@ function AIRBOSS:_Debrief(playerData) if not playerData.unit:InAir() then -- Welcome aboard! - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) + self:Sound2Player(playerData, self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) end @@ -10028,7 +10140,7 @@ end -- @field #number Tstarted Abs time when transmission began to play. -- @field #number prio Priority 0-100. -- @field #boolean isplaying Currently playing. --- @field Core.Beacon#RADIO radio Radio object. +-- @field #AIRBOSS.Radio radio Radio object. -- @field #AIRBOSS.RadioCall call Radio call. -- @field #boolean loud If true, play loud version of file. @@ -10097,7 +10209,7 @@ function AIRBOSS:_CheckRadioQueue(radioqueue, name) -- Found a new transmission. if next~=nil and not playing then - self:RadioTransmit(next.radio, next.call, next.loud) + self:Broadcast(next.radio, next.call, next.loud) next.isplaying=true next.Tstarted=time end @@ -10111,9 +10223,9 @@ function AIRBOSS:_CheckRadioQueue(radioqueue, name) end ---- Add Radio transmission to radio queue +--- Add Radio transmission to radio queue. -- @param #AIRBOSS self --- @param Core.Radio#RADIO radio sending transmission. +-- @param #AIRBOSS.Radio radio Radio sending the transmission. -- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. -- @param #boolean loud If true, play loud sound file version. -- @param #number delay Delay in seconds, before the message is broadcasted. @@ -10131,94 +10243,279 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay) transmission.Tstarted=nil transmission.loud=loud and call.loud + -- Player onboard number if sender has one. + if self:_IsOnboard(call.modexsender) then + self:_Number2Radio(radio, call.modexsender, delay) + end + + -- Play onboard number if receiver has one. + if self:_IsOnboard(call.modexreceiver) then + self:_Number2Radio(radio, call.modexreceiver, delay) + end + -- Add transmission to the right queue. - if radio:GetAlias()=="LSO" then + local caller="" + if radio.alias=="LSO" then table.insert(self.RQLSO, transmission) + + caller="LSOCall" - elseif radio:GetAlias()=="MARSHAL" then + elseif radio.alias=="MARSHAL" then table.insert(self.RQMarshal, transmission) + + caller="MarshalCall" end + + -- Append radio click sound at the end of the transmission. + if call~=AIRBOSS[caller].CLICK and + call~=AIRBOSS[caller].N0 and + call~=AIRBOSS[caller].N1 and + call~=AIRBOSS[caller].N2 and + call~=AIRBOSS[caller].N3 and + call~=AIRBOSS[caller].N4 and + call~=AIRBOSS[caller].N5 and + call~=AIRBOSS[caller].N6 and + call~=AIRBOSS[caller].N7 and + call~=AIRBOSS[caller].N8 and + call~=AIRBOSS[caller].N9 then + self:RadioTransmission(radio, AIRBOSS[caller].CLICK, false, delay) + end end ---- Transmission radio message. + +--- Check if a call needs a subtitle because the complete voice overs are not available. -- @param #AIRBOSS self --- @param Core.Radio#RADIO radio sending transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @return #boolean If true, call needs a subtitle. +function AIRBOSS:_NeedsSubtitle(call) + -- Currently we play the noise file. + if call.file==AIRBOSS.MarshalCall.NOISE.file or call.file==AIRBOSS.LSOCall.NOISE.file then + return true + else + return false + end +end + +--- Broadcast radio message. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio Radio sending transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud Play loud version of file. +function AIRBOSS:Broadcast(radio, call, loud) + self:F(call) + + -- Check which sound output method to use. + if not self.usersoundradio then + + ---------------------------- + -- Transmission via Radio -- + ---------------------------- + + -- Get unit sending the transmission. + local sender=self:_GetRadioSender() + + -- Construct file name and subtitle. + local filename=self:_RadioFilename(call, loud) + + -- Create subtitle for transmission. + local subtitle=self:_RadioSubtitle(radio, call, loud) + + -- Debug. + self:T({filename=filename, subtitle=subtitle}) + + if sender then + + -- Broadcasting from aircraft. Only players tuned in to the right frequency will see the message. + self:T(self.lid..string.format("Broadcasting from aircraft %s", sender:GetName())) + + -- Command to set the Frequency for the transmission. + local commandFrequency={ + id="SetFrequency", + params={ + frequency=radio.frequency*1000000, -- Frequency in Hz. + modulation=radio.modulation, + }} + + -- Command to tranmit the call. + local commandTransmit={ + id = "TransmitMessage", + params = { + file=filename, + duration=call.subduration or 5, + subtitle=subtitle, + loop=false, + }} + + -- Set commend for frequency + sender:SetCommand(commandFrequency) + + -- Set command for radio transmission. + sender:SetCommand(commandTransmit) + + else + + -- Broadcasting from carrier. No subtitle possible. Need to send messages to players. + self:T(self.lid..string.format("Broadcasting from carrier via trigger.action.radioTransmission().")) + + -- Transmit from carrier position. + local vec3=self.carrier:GetPositionVec3() + + -- Transmit via trigger. + trigger.action.radioTransmission(filename, vec3, radio.modulation, false, radio.frequency*1000000, 100) + + -- Display subtitle of message to players. + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Message to all players in CCA that have subtites on. + if playerData.unit:IsInZone(self.zoneCCA) and playerData.actype~=AIRBOSS.AircraftCarrier.A4EC then + + -- Only to players with subtitle on or if noise is played. + if playerData.subtitles or self:_NeedsSubtitle(call) then + + -- Messages to marshal to everyone. Messages on LSO radio only to those in the pattern. + if radio.alias=="MARSHAL" or (radio.alias=="LSO" and self:_InQueue(self.Qpattern, playerData.group)) then + + -- Message to player. + self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration or 5) + + end + + end + + end + end + end + end + + ---------------- + -- Easy Comms -- + ---------------- + + -- Workaround for the community A-4E-C as long as their radios are not functioning properly. + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Easy comms if globally activated but definitly for all player in the community A-4E. + if self.usersoundradio or playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + + -- Messages to marshal to everyone. Messages on LSO radio only to those in the pattern. + if radio.alias=="MARSHAL" or (radio.alias=="LSO" and self:_InQueue(self.Qpattern, playerData.group)) then + + -- User sound to players (inside CCA). + self:Sound2Player(playerData, radio, call, loud) + end + + end + end + +end + +--- Player user sound to player if he is inside the CCA. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #AIRBOSS.Radio radio The radio used for transmission. -- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. -- @param #boolean loud If true, play loud sound file version. -- @param #number delay Delay in seconds, before the message is broadcasted. -function AIRBOSS:RadioTransmit(radio, call, loud, delay) - self:F2({radio=radio, call=call, loud=loud, delay=delay}) +function AIRBOSS:Sound2Player(playerData, radio, call, loud, delay) - if (delay==nil) or (delay and delay==0) then + -- Only to players inside the CCA. + if playerData.unit:IsInZone(self.zoneCCA) and call then - -- Construct file name and subtitle. - local filename=call.file - local subtitle=call.subtitle - if loud then - if call.loud then - filename=filename.."_Loud" - end - if subtitle and subtitle~="" then - subtitle=subtitle.."!" - end - else - if subtitle and subtitle~="" then - subtitle=subtitle.."." - end + -- Construct file name. + local filename=self:_RadioFilename(call, loud) + + -- Get Subtitle + local subtitle=self:_RadioSubtitle(radio, call, loud) + + -- Play sound file via usersound trigger. + USERSOUND:New(filename):ToGroup(playerData.group, delay) + + -- Only to players with subtitle on or if noise is played. + if playerData.subtitles or self:_NeedsSubtitle(call) then + self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration or 10, false, delay) end - filename=filename.."."..(call.suffix or "ogg") - - -- Output via radio transmision or user sound. - if self.usersoundradio then - - for _,_player in pairs(self.players) do - local playerData=_player --#AIRBOSS.PlayerData - if playerData.unit:IsInZone(self.zoneCCA) then - USERSOUND:New(filename):ToGroup(playerData.group) - end - end - - else - - -- New transmission. - radio:NewUnitTransmission(filename, call.subtitle, call.duration, radio.Frequency/1000000, radio.Modulation, false) - - -- Broadcast message. - radio:Broadcast(true) - - -- Workaround for the community A-4E-C as long as their radios are not functioning properly. - for _,_player in pairs(self.players) do - local playerData=_player --#AIRBOSS.PlayerData - if playerData.actype==AIRBOSS.AircraftCarrier.A4EC then - USERSOUND:New(filename):ToGroup(playerData.group) - end - end - end - - -- Message "Subtitle" to all players. - for _,_player in pairs(self.players) do - local playerData=_player --#AIRBOSS.PlayerData - - -- Message to all players in CCA that have subtites on. - if playerData.unit:IsInZone(self.zoneCCA) and playerData.subtitles then - - -- Message to player. - self:MessageToPlayer(playerData, subtitle, radio:GetAlias(), "", call.duration) - - end - end - - else - - -- Scheduled transmission. - SCHEDULER:New(nil, self.RadioTransmit, {self, radio, call, loud}, delay) end end +--- Create radio subtitle from radio call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio The radio used for transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud If true, append "!" else ".". +-- @return #string Subtitle to be displayed. +function AIRBOSS:_RadioSubtitle(radio, call, loud) + + -- No subtitle if call is nil, or subtitle is nil or subtitle is empty. + if call==nil or call.subtitle==nil or call.subtitle=="" then + return "" + end + + -- Sender + local sender=call.sender or radio.alias + if call.modexsender then + sender=call.modexsender + end + + -- Modex of receiver. + local receiver=call.modexreceiver or "" + + -- Init subtitle. + local subtitle=string.format("%s: %s", sender, call.subtitle) + if receiver and receiver~="" then + subtitle=string.format("%s: %s, %s", sender, receiver, call.subtitle) + end + + -- Last character of the string. + local lastchar=string.sub(subtitle, -1) + + -- Append ! or . + if loud then + if lastchar=="." or lastchar=="!" then + subtitle=string.sub(subtitle, 1,-1) + end + subtitle=subtitle.."!" + else + if lastchar=="!" then + -- This also okay. + elseif lastchar=="." then + -- Nothing to do. + else + subtitle=subtitle.."." + end + end + + return subtitle +end + +--- Get full file name for radio call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud Use loud version of file if available. +-- @return #string The file name of the radio sound. +function AIRBOSS:_RadioFilename(call, loud) + + -- Construct file name and subtitle. + local prefix=call.file or "" + local suffix=call.suffix or "ogg" + local path="l10n/DEFAULT/" + + -- Loud version. + if loud then + prefix=prefix.."_Loud" + end + + -- File name inclusing path in miz file. + local filename=string.format("%s%s.%s", path, prefix, suffix) + + return filename +end + --- Send text message to player client. -- Message format will be "SENDER: RECCEIVER, MESSAGE". -- @param #AIRBOSS self @@ -10254,23 +10551,17 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear, 0, soundoff}, delay) else - if not soundoff then + if receiver==playerData.onboard and not soundoff then - if receiver=="99" then + -- Sound only to player group. + if sender and (sender=="LSO" or sender=="MARSHAL" or sender=="AIRBOSS") then - -- Radio message from LSO or MARSHAL to all. - if sender and sender=="LSO" then - self:_Number2Radio(self.LSORadio, receiver, delay) - elseif sender and (sender=="MARSHAL" or sender=="AIRBOSS") then - self:_Number2Radio(self.MarshalRadio, receiver, delay) - end - - elseif receiver==playerData.onboard then - - -- Sound only to player group. - if sender and (sender=="LSO" or sender=="MARSHAL" or sender=="AIRBOSS") then - self:_Number2Sound(playerData, sender, receiver, delay) - end + -- User sound of board number. + local wait=self:_Number2Sound(playerData, sender, receiver) + + -- Play click sound to end message. + local filename=self:_RadioFilename(AIRBOSS.MarshalCall.CLICK) + USERSOUND:New(filename):ToGroup(playerData.group, wait) end end @@ -10295,40 +10586,17 @@ end -- @param #number duration Display message duration. Default 10 seconds. -- @param #boolean clear If true, clear screen from previous messages. -- @param #number delay Delay in seconds, before the message is displayed. --- @param #boolean soundoff If true, do not play boad number message. -function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, delay, soundoff) +function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, delay) -- Local delay. local _delay=delay or 0 - -- Broadcast onboard number on Marshal frequency if sender. - if self:_IsOnboard(sender) then - self:_Number2Radio(self.LSORadio, sender, _delay) - _delay=_delay+3 - end - - if self:_IsOnboard(receiver) or receiver=="99" then - self:_Number2Radio(self.LSORadio, receiver, _delay) - end + -- Create new (fake) radio call to show the subtitile. + local call=self:_NewRadioCall(AIRBOSS.LSOCall.NOISE, sender or "LSO", message, duration, receiver, sender) + + -- Dummy radio transmission to display subtitle only to those who tuned in. + self:RadioTransmission(self.LSORadio, call, false, delay) - -- Loop over all flights in the pattern queue. - for _,_player in pairs(self.players) do - local player=_player --#AIRBOSS.PlayerData - - - -- Only to players inside CCA. - if player and player.unit:IsInZone(self.zoneCCA) then - - -- To all if adressed to 99 or only to flights in the pattern queue. - if receiver=="99" or self:_InQueue(self.Qpattern, player.group) then - - -- Message to player. - self:MessageToPlayer(player, message, sender, receiver, duration, clear, delay, true) - - end - - end - end end --- Send text message to all players in the marshal queue. @@ -10345,34 +10613,66 @@ function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, de -- Local delay. local _delay=delay or 0 - -- Broadcast onboard number on Marshal frequency if sender is onboard. - if self:_IsOnboard(sender) then - self:_Number2Radio(self.MarshalRadio, sender, _delay) - _delay=_delay+3 - end + -- Create new (fake) radio call to show the subtitile. + local call=self:_NewRadioCall(AIRBOSS.MarshalCall.NOISE, sender or "MARSHAL", message, duration, receiver, sender) + + -- Dummy radio transmission to display subtitle only to those who tuned in. + self:RadioTransmission(self.MarshalRadio, call, false, delay) - -- Broadcast onboard number on Marshal frequency if receiver is onboard. - if self:_IsOnboard(receiver) or receiver=="99" then - self:_Number2Radio(self.MarshalRadio, receiver, _delay) +end + +--- Generate a new radio call (deepcopy) from an existing default call. P +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall call Radio call to be enhanced. +-- @param #string sender Sender of the message. Default is the radio alias. +-- @param #string subtitle Subtitle of the message. Default from original radio call. Use "" for no subtitle. +-- @param #number subduration Time in seconds the subtitle is displayed. Default 10 seconds. +-- @param #string modexreceiver Onboard number of the receiver or nil. +-- @param #string modexsender Onboard number of the sender or nil. +function AIRBOSS:_NewRadioCall(call, sender, subtitle, subduration, modexreceiver, modexsender) + + -- Create a new call + local newcall=UTILS.DeepCopy(call) --#AIRBOSS.RadioCall + + -- Sender for displaying the subtitle. + newcall.sender=sender + + -- Subtitle of the message. + newcall.subtitle=subtitle or call.subtitle + + -- Duration of subtitle display. + newcall.subduration=subduration or 10 + + -- Tail number of the receiver. + if self:_IsOnboard(modexreceiver) then + newcall.modexreceiver=modexreceiver end - - -- Loop over all flights in the marshal queue. - for _,_player in pairs(self.players) do - local player=_player --#AIRBOSS.PlayerData - - -- Only to players inside CCA. - if player and player.unit:IsInZone(self.zoneCCA) then - - -- To all if adressed to 99 or only to flights in the Marshal or Waiting queue. - --if receiver=="99" or self:_InQueue(self.Qmarshal, player.group) or self:_InQueue(self.Qwaiting, player.group) then - - -- Message to player. - self:MessageToPlayer(player, message, sender, receiver, duration, clear, delay, true) - - --end - - end + + -- Tail number of the sender. + if self:_IsOnboard(modexsender) then + newcall.modexsender=modexsender end + + return newcall +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +-- @param #AIRBOSS self +-- @return Wrapper.Unit#UNIT Sending aircraft unit or nil if was not setup, is not an aircraft or is not alive. +function AIRBOSS:_GetRadioSender() + + -- Check if we have a sending aircraft. + local sender=nil --Wrapper.Unit#UNIT + if self.senderac then + sender=UNIT:FindByName(self.senderac) + end + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() and sender:IsAir() then + return sender + end + + return nil end --- Check if text is an onboard number of a flight. @@ -10385,6 +10685,11 @@ function AIRBOSS:_IsOnboard(text) if text==nil then return false end + + -- Message to all. + if text=="99" then + return true + end -- Loop over all flights. for _,_flight in pairs(self.flights) do @@ -10408,8 +10713,12 @@ end -- @param #string sender Who is sending the call, either "LSO" or "MARSHAL". -- @param #string number Number string, e.g. "032" or "183". -- @param #number delay Delay before transmission in seconds. +-- @return #number Duration of the call in seconds. function AIRBOSS:_Number2Sound(playerData, sender, number, delay) + -- Default. + delay=delay or 0 + --- Split string into characters. local function _split(str) local chars={} @@ -10420,55 +10729,52 @@ function AIRBOSS:_Number2Sound(playerData, sender, number, delay) return chars end - if delay and delay>0 then - -- Delayed call. - SCHEDULER:New(nil, AIRBOSS._Number2Sound, {self, playerData, sender, number}, delay) + -- Sender + local Sender + if sender=="LSO" then + Sender="LSOCall" + elseif sender=="MARSHAL" or sender=="AIRBOSS" then + Sender="MarshalCall" else - - -- Split string into characters. - local numbers=_split(number) - - local Sender - if sender=="LSO" then - Sender="LSOCall" - elseif sender=="MARSHAL" or sender=="AIRBOSS" then - Sender="MarshalCall" - else - self:E(self.lid..string.format("ERROR: Unknown radio sender %s!", tostring(sender))) - return - end - - local wait=0 - for i=1,#numbers do - - -- Current number - local n=numbers[i] - - -- Convert to N0, N1, ... - local N=string.format("N%s", n) - - -- Radio call. - local call=AIRBOSS[Sender][N] --#AIRBOSS.RadioCall - - -- Create file name. - local filename=string.format("%s.%s", call.file, call.suffix) - - -- Play sound. - USERSOUND:New(filename):ToGroup(playerData.group, wait) - - -- Wait until this call is over before playing the next. - wait=wait+call.duration - end - + self:E(self.lid..string.format("ERROR: Unknown radio sender %s!", tostring(sender))) + return end + + -- Split string into characters. + local numbers=_split(number) + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Convert to N0, N1, ... + local N=string.format("N%s", n) + + -- Radio call. + local call=AIRBOSS[Sender][N] --#AIRBOSS.RadioCall + + -- Create file name. + local filename=string.format("%s.%s", call.file, call.suffix) + + -- Play sound. + USERSOUND:New(filename):ToGroup(playerData.group, delay+wait) + + -- Wait until this call is over before playing the next. + wait=wait+call.duration + end + + return wait end --- Convert a number (as string) into a radio message. -- E.g. for board number or headings. -- @param #AIRBOSS self --- @param Core.Radio#RADIO radio Radio used for transmission. +-- @param #AIRBOSS.Radio radio Radio used for transmission. -- @param #string number Number string, e.g. "032" or "183". -- @param #number delay Delay before transmission in seconds. +-- @return #number Duration of the call in seconds. function AIRBOSS:_Number2Radio(radio, number, delay) --- Split string into characters. @@ -10481,51 +10787,40 @@ function AIRBOSS:_Number2Radio(radio, number, delay) return chars end - -- Get radio alias. - local alias=radio:GetAlias() - - local sender="" - if alias=="LSO" then - sender="LSOCall" - elseif alias=="MARSHAL" then - sender="MarshalCall" + -- Sender. + local Sender="" + if radio.alias=="LSO" then + Sender="LSOCall" + elseif radio.alias=="MARSHAL" then + Sender="MarshalCall" else - self:E(self.lid.."ERROR: Unknown radio alias!") + self:E(self.lid..string.format("ERROR: Unknown radio alias %s!", tostring(radio.alias))) end -- Split string into characters. local numbers=_split(number) - + + local wait=0 for i=1,#numbers do -- Current number local n=numbers[i] - if n=="0" then - self:RadioTransmission(radio, AIRBOSS[sender].N0, false, delay) - elseif n=="1" then - self:RadioTransmission(radio, AIRBOSS[sender].N1, false, delay) - elseif n=="2" then - self:RadioTransmission(radio, AIRBOSS[sender].N2, false, delay) - elseif n=="3" then - self:RadioTransmission(radio, AIRBOSS[sender].N3, false, delay) - elseif n=="4" then - self:RadioTransmission(radio, AIRBOSS[sender].N4, false, delay) - elseif n=="5" then - self:RadioTransmission(radio, AIRBOSS[sender].N5, false, delay) - elseif n=="6" then - self:RadioTransmission(radio, AIRBOSS[sender].N6, false, delay) - elseif n=="7" then - self:RadioTransmission(radio, AIRBOSS[sender].N7, false, delay) - elseif n=="8" then - self:RadioTransmission(radio, AIRBOSS[sender].N8, false, delay) - elseif n=="9" then - self:RadioTransmission(radio, AIRBOSS[sender].N9, false, delay) - else - self:E(self.lid..string.format("ERROR: Unknown number %s!", tostring(n))) - end + -- Convert to N0, N1, ... + local N=string.format("N%s", n) + + -- Radio call. + local call=AIRBOSS[Sender][N] --#AIRBOSS.RadioCall + + -- Transmit. + self:RadioTransmission(radio, call, false, delay) + + -- Add up duration of the number. + wait=wait+call.duration end - + + -- Return the total duration of the call. + return wait end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -10761,6 +11056,7 @@ function AIRBOSS:_RequestCommence(_unitName) -- Check if unit is in CCA. local text="" + local cleared=false if _unit:IsInZone(self.zoneCCA) then -- Get stack value. @@ -10849,9 +11145,8 @@ function AIRBOSS:_RequestCommence(_unitName) table.insert(self.Qpattern, playerData) end - -- Call commence routine. No zone check. - -- NOTE: Commencing will set step for all section members as well. - self:_Commencing(playerData, false) + -- Clear player for commence. + cleared=true end else @@ -10864,6 +11159,13 @@ function AIRBOSS:_RequestCommence(_unitName) -- Send message. self:MessageToPlayer(playerData, text, "MARSHAL") + + -- Check if player was cleard. Need to do this after the message above is displayed. + if cleared then + -- Call commence routine. No zone check. + -- NOTE: Commencing will set step for all section members as well. + self:_Commencing(playerData, false) + end end end end @@ -11458,7 +11760,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) text=text..string.format("Case %d recovery ops\n", self.case) else local radial=self:GetRadial(self.case, true, true, true) - text=text..string.format("Case %d recovery ops\nMarshal radial %03d°)\n", self.case, radial) + text=text..string.format("Case %d recovery ops\nMarshal radial %03d°\n", self.case, radial) end text=text..string.format("BRC %03d°\n", self:GetBRC()) text=text..string.format("FB %03d°\n", self:GetFinalBearing(true)) @@ -11518,18 +11820,6 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) local WD=string.format('%03d°', Wd) local Ts=string.format("%d°C",T) - --[[ - 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", UTILS.hPa2mmHg(P)) - 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", UTILS.hPa2inHg(P)) - end - ]] - local tT=string.format("%d°C",T) local tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) local tP=string.format("%.2f inHg", UTILS.hPa2inHg(P)) @@ -11796,16 +12086,16 @@ function AIRBOSS:_MarkMarshalZone(_unitName, flare) zoneHolding:FlareZone(FLARECOLOR.White, 45, nil, patternalt) if playerData.case==1 then - text=text.."\nMarking Commence zone with GREEN flares." - zoneThree:FlareZone(FLARECOLOR.Green, 45, nil, patternalt) + text=text.."\nMarking Commence zone with RED flares." + zoneThree:FlareZone(FLARECOLOR.Red, 45, nil, patternalt) end else text="Marking Marshal zone with WHITE smoke." zoneHolding:SmokeZone(SMOKECOLOR.White, 45, patternalt) if playerData.case==1 then - text=text.."\nMarking Commence zone with GREEN smoke." - zoneThree:SmokeZone(SMOKECOLOR.Green, 45, patternalt) + text=text.."\nMarking Commence zone with RED smoke." + zoneThree:SmokeZone(SMOKECOLOR.Red, 45, patternalt) end end @@ -11857,8 +12147,8 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) -- Case II/III: approach corridor if case==2 or case==3 then - text=text.."* approach corridor with YELLOW flares\n" - self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Yellow, 45) + text=text.."* approach corridor with GREEN flares\n" + self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) end -- Case II/III: platform @@ -11903,8 +12193,8 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) -- Case II/III: Approach Corridor if case==2 or case==3 then - text=text.."* approach corridor with ORANGE smoke\n" - self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Orange, 45) + text=text.."* approach corridor with GREEN smoke\n" + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) end -- Case II/III: platform diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index c4877d704..398745c7f 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -266,7 +266,7 @@ RECOVERYTANKER.version="1.0.3" -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Is alive check for tanker necessary? +-- DONE: Is alive check for tanker necessary? -- DONE: Seamless change of position update. Get good updated waypoint and update position if tanker position is right. Not really possiple atm. -- DONE: Check if TACAN mode "X" is allowed for AA TACAN stations. Nope -- DONE: Check if tanker is going back to "Running" state after RTB and respawn. @@ -708,6 +708,24 @@ function RECOVERYTANKER:IsStopped() return self:is("Stopped") end +--- Alias of tanker spawn group. +-- @param #RECOVERYTANKER self +-- @return #string Alias of the tanker. +function RECOVERYTANKER:GetAlias() + return self.alias +end + +--- Get unit name of the spawned tanker. +-- @param #RECOVERYTANKER self +-- @return #string Name of the tanker unit or nil if it does not exist. +function RECOVERYTANKER:GetUnitName() + local unit=self.tanker:GetUnit(1) + if unit then + return unit:GetName() + end + return nil +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM states ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index b489be76b..0142bb201 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -651,6 +651,17 @@ function RESCUEHELO:GetAlias() return self.alias end +--- Get unit name of the spawned helo. +-- @param #RESCUEHELO self +-- @return #string Name of the helo unit or nil if it does not exist. +function RESCUEHELO:GetUnitName() + local unit=self.helo:GetUnit(1) + if unit then + return unit:GetName() + end + return nil +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- EVENT functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -909,7 +920,7 @@ function RESCUEHELO:onafterStatus(From, Event, To) -- Report current fuel. local text=string.format("Rescue Helo %s: state=%s fuel=%.1f", self.helo:GetName(), self:GetState(), fuel) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) - self:E(self.lid..text) + self:T(self.lid..text) if self:IsRunning() then From 8f3aeeb18217d467e80077b80a97bf4dae08d33d Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 26 Jan 2019 23:12:24 +0100 Subject: [PATCH 139/147] AIRBOSS v0.92 --- Moose Development/Moose/Ops/Airboss.lua | 1004 +++++++++++++++++------ 1 file changed, 745 insertions(+), 259 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index df2091a06..5925d9b40 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -9,6 +9,7 @@ -- * Automatic LSO grading including (optional) live grading while in the groove. -- * Different skill levels from on-the-fly tips for flight students to *ziplip* for pros. Can be set for each player individually. -- * Define recovery time windows with individual recovery cases in the same mission. +-- * Option to let the carrier steam into the wind automatically. -- * Automatic TACAN and ICLS channel setting of carrier. -- * Separate radio channels for LSO and Marshal transmissions. -- * Voice over support for LSO and Marshal radio transmissions. @@ -43,6 +44,13 @@ -- -- **PLEASE NOTE** that his class is work in progress and in an early **alpha** stage. Many/most things work already very nicely but there a lot of cases I did not run into yet. -- Therefore, your *constructive* feedback is both necessary and appreciated! +-- +-- ## IMPORTANT +-- +-- Due to technical restrictions of DCS make sure you have: +-- +-- * Each player slot in a separate group. DCS does only allow to send messages to groups and not to individual units. +-- * Players are identified by their player name. Ensure that no two player have the same name, e.g. "New Callsign", as this will lead to unexpected results. -- -- ### Open Questions? -- @@ -149,6 +157,12 @@ -- @field #number windowcount Running number counting the recovery windows. -- @field #number LSOdT Time interval in seconds before the LSO will make its next call. -- @field #string senderac Name of the aircraft acting as sender for broadcasting radio messages from the carrier. DCS shortcoming workaround. +-- @field #boolean turnintowind If true, carrier is currently turning into the wind. +-- @field #boolean detour If true, carrier is currently making a detour from its path along the ME waypoints. +-- @field Core.Point#COORDINATE Creturnto Position to return to after turn into the wind leg is over. +-- @field Core.Set#SET_GROUP squadsetAI AI groups in this set will be handled by the airboss. +-- @field #boolean menusingle If true, menu is optimized for a single carrier. +-- @field #number collisiondist Distance up to which collision checks are done. -- @extends Core.Fsm#FSM --- Be the boss! @@ -232,6 +246,10 @@ -- The F10 radio menu can be used to post requests to Marshal but also provides information about the player and carrier status. Additionally, helper functions -- can be called. -- +-- By default, the script creates a submenu "Airboss" in the "F10 Other ..." menu and each @{#AIRBOSS} carrier gets its own submenu. +-- If you intend to have only one carrier, you can simplify the menu structure using the @{#AIRBOSS.SetMenuSingleCarrier} function, which will create all carrier specific menu entries directly +-- in the "Airboss" submenu. (Needless to say, that if you enable this and define mulitiple carriers, the menu structure will get completely screwed up.) +-- -- ## Root Menu -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuRoot.png) @@ -561,6 +579,20 @@ -- AddRecoveryWindow(string.format("08:05:00+%d", i), string.format("08:50:00+%d", i)) -- end -- +-- ### Turning into the Wind +-- +-- For each recovery window, you can define if the carrier should automatically turn into the wind. This is done by passing one or two additional arguments to the @{#AIRBOSS.AddRecoveryWindow} function: +-- +-- airbossStennis:AddRecoveryWindow("8:30", "9:30", 1, nil, true, 20) +-- +-- Setting the fifth parameter to *true* enables the automatic turning into the wind. The sixth parameter (here 20) specifies the speed in knots the carrier will go. +-- The carrier will steam into the wind for as long as the recovery window is open. The distance up to which possible collisions are detected can be set by the @{#AIRBOSS.SetCollisionDistance} function. +-- +-- However, the airboss scans the type of the surface up to 5 NM in the direction of movement of the carrier. If he detects anything but deep water, he will stop the current course and head back to +-- the point where he initially turned into the wind. +-- +-- The same holds true after the recovery window closes. The carrier will head back to the place where he left its assigned route and resume the path to the next waypoint defined in the mission editor. +-- -- === -- -- # Persistence of Player Results @@ -818,6 +850,11 @@ AIRBOSS = { windowcount = 0, LSOdT = nil, senderac = nil, + turnintowind = nil, + detour = nil, + squadsetAI = nil, + menusingle = nil, + collisiondist = nil, } --- Player aircraft types capable of landing on carriers. @@ -1388,6 +1425,8 @@ AIRBOSS.Difficulty={ -- @field #number OFFSET Angle offset of the holding pattern in degrees. Usually 0, +-15, or +-30 degrees. -- @field #boolean OPEN Recovery window is currently open. -- @field #boolean OVER Recovery window is over and closed. +-- @field #boolean WIND Carrier will turn into the wind. +-- @field #number SPEED The speed in knots the carrier has during the recovery. -- @field #number ID Recovery window ID. --- Groove data. @@ -1487,7 +1526,7 @@ AIRBOSS.MenuF10={} --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.1" +AIRBOSS.version="0.9.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1661,7 +1700,10 @@ function AIRBOSS:New(carriername, alias) -- Carrier patrols its waypoints until the end of time. self:SetPatrolAdInfinitum(true) - + + -- Collision check distance. Default 5 NM. + self:SetCollisionDistance() + -- Set update time intervals. self:SetQueueUpdateTime() self:SetStatusUpdateTime() @@ -1669,6 +1711,7 @@ function AIRBOSS:New(carriername, alias) -- Menu options. self:SetMenuMarkZones() self:SetMenuSmokeZones() + self:SetMenuSingleCarrier(false) -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then @@ -1693,7 +1736,7 @@ function AIRBOSS:New(carriername, alias) -- Debug trace. if false then - self.Debug=false + self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) @@ -2008,6 +2051,15 @@ function AIRBOSS:SetCarrierControlledZone(radius) return self end +--- Set distance up to which water ahead is scanned for collisions. +-- @param #AIRBOSS self +-- @param #number dist Distance in NM. Default 5 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCollisionDistance(distance) + self.collisiondist=UTILS.NMToMeters(distance or 5) + return self +end + --- Set the default recovery case. -- @param #AIRBOSS self -- @param #number case Case of recovery. Either 1, 2 or 3. Default 1. @@ -2046,8 +2098,10 @@ end -- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. -- @param #number case Recovery case for that time slot. Number between one and three. -- @param #number holdingoffset Only for CASE II/III: Angle in degrees the holding pattern is offset. +-- @param #boolean turnintowind If true, carrier will turn into the wind 5 minutes before the recovery window opens. +-- @param #number speed Speed in knots during turn into wind leg. -- @return #AIRBOSS.Recovery Recovery window. -function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset) +function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset, turnintowind, speed) -- Absolute mission time in seconds. local Tnow=timer.getAbsTime() @@ -2093,6 +2147,8 @@ function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset) recovery.OFFSET=holdingoffset recovery.OPEN=false recovery.OVER=false + recovery.WIND=turnintowind + recovery.SPEED=speed or 20 recovery.ID=self.windowcount -- Add to table @@ -2101,6 +2157,15 @@ function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset) return recovery end +--- Define a set of AI groups that are handled by the airboss. +-- @param #AIRBOSS self +-- @param Core.Set#SET_GROUP setgroup The set of AI groups which are handled by the airboss. +-- @return #AIRBOSS self +function AIRBOSS:SetSquadronAI(setgroup) + self.squadsetAI=setgroup + return self +end + --- Close currently running recovery window and stop recovery ops. Recovery window is deleted. -- @param #AIRBOSS self -- @param #number delay (Optional) Delay in seconds before the window is deleted. @@ -2166,7 +2231,6 @@ function AIRBOSS:DeleteRecoveryWindow(window, delay) if window.OPEN then -- Window is currently open. self:RecoveryStop() - --self:CloseCurrentRecoveryWindow() else table.remove(self.recoverytimes, i) end @@ -2226,6 +2290,20 @@ function AIRBOSS:SetMarshalRadius(radius) return self end +--- Optimized F10 radio menu for a single carrier. The menu entries will be stored directly under F10 Other/Airboss/ and not F10 Other/Airboss/"Carrier Alias"/. +-- **WARNING**: If you use this with two airboss objects/carriers, the radio menu will be screwed up! +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil single menu is enabled. If false, menu is for multiple carriers in the mission. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuSingleCarrier(switch) + if switch==true or switch==nil then + self.menusingle=true + else + self.menusingle=false + end + return self +end + --- Enable or disable F10 radio menu for marking zones via smoke or flares. -- @param #AIRBOSS self -- @param #boolean switch If true or nil, menu is enabled. If false, menu is not available to players. @@ -2647,12 +2725,46 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Get time. local clock=UTILS.SecondsToClock(timer.getAbsTime()) + local eta=UTILS.SecondsToClock(self:_GetETAatNextWP()) + + -- Current heading and position of the carrier. + local hdg=self:GetHeading() + local pos=self:GetCoordinate() + + -- Check water is ahead. + local collision=self:_CheckCollisionCoord(pos:Translate(self.collisiondist, hdg)) -- Debug info. - local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s", - clock, self:GetState(), self.case, self.carrier:GetVelocityKNOTS(), self:GetHeading(), self.currentwp, UTILS.SecondsToClock(self:_GetETAatNextWP())) + local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s - Collision Warning=%s", + clock, self:GetState(), self.case, self.carrier:GetVelocityKNOTS(), hdg, self.currentwp, eta, tostring(collision)) self:T(self.lid..text) + -- Check for collision. + if collision then + + -- We are currently turning into the wind. + if self.turnintowind then + + -- Carrier resumes its initial route. This disables turnintowind switch. + self:CarrierResumeRoute(self.Creturnto) + + -- Since current window would stay open, we disable the WIND switch. + if self:IsRecovering() and self.recoverywindow and self.recoverywindow.WIND then + -- Disable turn into the wind for this window so that we do not do this all over again. + self.recoverywindow.WIND=false + end + + else + + -- Find path around the obstacle. + if not self.detour then + --self:_Pathfinder() + end + + end + end + + -- Check recovery times and start/stop recovery mode if necessary. self:_CheckRecoveryTimes() @@ -2939,7 +3051,28 @@ function AIRBOSS:_CheckRecoveryTimes() if nextwindow then -- Set case and offset of the next window. self:RecoveryCase(nextwindow.CASE, nextwindow.OFFSET) + + -- Check if time is less than 5 minutes. + if nextwindow.WIND and nextwindow.START-time<5*60 and not self.turnintowind then + + -- Calculate distance we travel into the wind. + local t=nextwindow.STOP-nextwindow.START + local v=UTILS.KnotsToMps(nextwindow.SPEED) + local s=v*t + + -- Distance in NM to go + 1 NM safety. + local d=UTILS.MetersToNM(s)+1 + + -- Route carrier into the wind. Sets self.turnintowind=true + self:CarrierTurnIntoWind(d, nextwindow.SPEED) + + -- Set wind switch to false. + --nextwindow.WIND=false + end + + -- Set current recovery window. self.recoverywindow=nextwindow + else -- No next window. Set default values. self:RecoveryCase() @@ -3104,7 +3237,12 @@ function AIRBOSS:onafterRecoveryStop(From, Event, To) local text=string.format("Case %d recovery ops are stopped.", self.case) -- Message to Marshal. - self:MessageToMarshal(text, "AIRBOSS", "99") + self:MessageToMarshal(text, "AIRBOSS", "99") + + -- If carrier is currently heading into the wind, we resume the original route. + if self.turnintowind then + self:CarrierResumeRoute(self.Creturnto) + end -- Check recovery windows. This sets self.recoverywindow to the next window. self:_CheckRecoveryTimes() @@ -3180,147 +3318,6 @@ end -- Parameter initialization ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Function called when a group is passing a waypoint. ---@param Wrapper.Group#GROUP group Group that passed the waypoint ---@param #AIRBOSS airboss Airboss object. ---@param #number i Waypoint number that has been reached. ---@param #number final Final waypoint number. -function AIRBOSS._PassingWaypoint(group, airboss, i, final) - - -- Debug message. - local text=string.format("Group %s passing waypoint %d of %d.", group:GetName(), i, final) - - -- Debug smoke and marker. - if airboss.Debug and false then - local pos=group:GetCoordinate() - pos:SmokeRed() - local MarkerID=pos:MarkToAll(string.format("Group %s reached waypoint %d", group:GetName(), i)) - end - - -- Debug message. - MESSAGE:New(text,10):ToAllIf(airboss.Debug) - airboss:T(airboss.lid..text) - - -- Set current waypoint. - airboss.currentwp=i - - -- If final waypoint reached, do route all over again. - if i==final and final>1 and airboss.adinfinitum then - airboss:_PatrolRoute() - end -end - ---- Function called when a group has reached the holding zone. ---@param Wrapper.Group#GROUP group Group that reached the holding zone. ---@param #AIRBOSS airboss Airboss object. ---@param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone. -function AIRBOSS._ReachedHoldingZone(group, airboss, flight) - - -- Debug message. - local text=string.format("Flight %s reached holding zone.", group:GetName()) - MESSAGE:New(text,10):ToAllIf(airboss.Debug) - airboss:T(airboss.lid..text) - - -- Debug mark. - if airboss.Debug then - group:GetCoordinate():MarkToAll(text) - end - - -- Set holding flag true and set timestamp for marshal time check. - if flight then - flight.holding=true - flight.time=timer.getAbsTime() - end -end - - ---- Patrol carrier --- @param #AIRBOSS self --- @return #AIRBOSS self -function AIRBOSS:_PatrolRoute() - - -- Get carrier group. - local CarrierGroup=self.carrier:GetGroup() - - -- Waypoints of group. - local Waypoints = CarrierGroup:GetTemplateRoutePoints() - - -- NOTE: This is only necessary, if the first waypoint would already be far way, i.e. when the script is started with a large delay. - -- Calculate the new Route. - --local wp0=CarrierGroup:GetCoordinate():WaypointGround(5.5*3.6) - -- Insert current coordinate as first waypoint - --table.insert(Waypoints, 1, wp0) - - for n=1,#Waypoints do - - -- Passing waypoint taskfunction - local TaskPassingWP=CarrierGroup:TaskFunction("AIRBOSS._PassingWaypoint", self, n, #Waypoints) - - -- Call task function when carrier arrives at waypoint. - CarrierGroup:SetTaskWaypoint(Waypoints[n], TaskPassingWP) - end - - -- Set waypoint table. - local i=1 - for _,point in ipairs(Waypoints) do - - -- Coordinate of the waypoint - local coord=COORDINATE:New(point.x, point.alt, point.y) - - -- Set velocity of the coordinate. - coord:SetVelocity(point.speed) - - -- Add to table. - table.insert(self.waypoints, coord) - - -- Debug info. - if self.Debug then - coord:MarkToAll(string.format("Carrier Waypoint %d, Speed=%.1f knots", i, UTILS.MpsToKnots(point.speed))) - end - - -- Increase counter. - i=i+1 - end - - -- Current waypoint is 1. - self.currentwp=1 - - -- Route carrier group. - CarrierGroup:Route(Waypoints) -end - ---- Estimated the carrier position at some point in the future given the current waypoints and speeds. --- @param #AIRBOSS self --- @return DCS#time ETA abs. time in seconds. -function AIRBOSS:_GetETAatNextWP() - - -- Current waypoint - local cwp=self.currentwp - - -- Current abs. time. - local tnow=timer.getAbsTime() - - -- Current position. - local p=self:GetCoordinate() - - -- Current velocity [m/s]. - local v=self.carrier:GetVelocityMPS() - - -- Distance to next waypoint. - local s=0 - if #self.waypoints>cwp then - s=p:Get2DDistance(self.waypoints[cwp+1]) - end - - -- v=s/t <==> t=s/v - local t=s/v - - -- ETA - local eta=t+tnow - - return eta -end - --- Init parameters for USS Stennis carrier. -- @param #AIRBOSS self function AIRBOSS:_InitStennis() @@ -4037,6 +4034,17 @@ function AIRBOSS:_ScanCarrierZone() -- Check if flight is AI and if we want to handle it at all. if knownflight.ai and self.handleai then + -- Check if AI group is part of the group set if a set was defined. + local iscarriersquad=true + if self.squadsetAI then + local group=self.squadsetAI:FindGroup(groupname) + if group then + iscarriersquad=true + else + iscarriersquad=false + end + end + -- Get distance to carrier. local dist=knownflight.group:GetCoordinate():Get2DDistance(self:GetCoordinate()) @@ -4047,7 +4055,7 @@ function AIRBOSS:_ScanCarrierZone() self:T3(self.lid..string.format("Known AI flight group %s closed in by %.1f NM", knownflight.groupname, UTILS.MetersToNM(closein))) -- Send AI flight to marshal stack if group closes in more than 5 and has initial flag value. - if closein>UTILS.NMToMeters(5) and knownflight.flag:Get()==-100 then + if closein>UTILS.NMToMeters(5) and knownflight.flag:Get()==-100 and iscarriersquad then -- Check that we do not add a recovery tanker for marshaling. if self.tanker and self.tanker.tanker:GetName()==groupname then @@ -4642,8 +4650,15 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) -- Stack altitude. local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case)) + + -- Current BRC. local brc=self:GetBRC() + -- If the carrier is supposed to turn into the wind, we take the wind coordinate. + if self.recoverywindow and self.recoverywindow.WIND then + brc=self:GetBRCintoWind() + end + -- Get charlie time estimate. flight.Tcharlie=self:_GetCharlieTime(flight) @@ -4651,7 +4666,7 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) local Ccharlie=UTILS.SecondsToClock(flight.Tcharlie) -- Marshal message. - local text=string.format("Case %d, BRC is %03d°, hold at %d. Expected Charlie Time %s.\n", flight.case, brc, alt, tostring(Ccharlie)) + local text=string.format("Case %d, expected BRC %03d°, hold at %d. Expected Charlie Time %s.\n", flight.case, brc, alt, tostring(Ccharlie)) text=text..string.format("Altimeter %.2f. Report see me.", P) -- Message to all players. @@ -5209,7 +5224,7 @@ function AIRBOSS:_InitPlayer(playerData, step) -- Set us up on final if group name contains "Groove". But only for the first pass. if playerData.group:GetName():match("Groove") and playerData.passes==0 then self:MessageToPlayer(playerData, "Group name contains \"Groove\". Happy groove testing.") - playerData.attitudemonitor=false + playerData.attitudemonitor=true playerData.step=AIRBOSS.PatternStep.FINAL table.insert(self.Qpattern, playerData) end @@ -5475,109 +5490,6 @@ function AIRBOSS:_RemoveFlight(flight, completely) end ---- Check if heading or position of carrier have changed significantly. --- @param #AIRBOSS self -function AIRBOSS:_CheckPatternUpdate() - - -- TODO: Make parameters input values. - - -- Min 10 min between pattern updates. - local dTPupdate=10*60 - - -- Update if carrier moves by more than 2.5 NM. - local Dupdate=UTILS.NMToMeters(2.5) - - -- Update if carrier turned by more than 5 degrees. - local Hupdate=5 - - -- Time since last pattern update - local dt=timer.getTime()-self.Tpupdate - - -- At least 10 min between updates. Not yet... - if dt=1 - - -- No update if carrier is turning! - if turning then - self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) - return - end - - -- Check if orientation changed. - local Hchange=false - if math.abs(deltaHeading)>=Hupdate then - self:T(self.lid..string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) - Hchange=true - end - - -- Get distance to saved position. - local dist=pos:Get2DDistance(self.Cposition) - - -- Check if carrier moved more than ~10 km. - local Dchange=false - if dist>=Dupdate then - self:T(self.lid..string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) - Dchange=true - end - - -- If heading or distance changed ==> update marshal AI patterns. - if Hchange or Dchange then - - -- Loop over all marshal flights - for _,_flight in pairs(self.Qmarshal) do - local flight=_flight --#AIRBOSS.FlightGroup - - -- Update marshal pattern of AI keeping the same stack. - if flight.ai then - self:_MarshalAI(flight, flight.flag:Get()) - end - - end - - -- Inform player about new final bearing. - if Hchange then - -- 99, new final bearing XXX - local FB=self:GetFinalBearing(true) - local text=string.format("new final bearing %03d°.", FB) - self:MessageToMarshal(text, "AIRBOSS", "99", 10) - end - - -- Reset parameters for next update check. - self.Corientation=vNew - self.Cposition=pos - self.Tpupdate=timer.getTime() - end - -end - ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Player Status ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -8453,6 +8365,40 @@ function AIRBOSS:GetBRC() return self:GetHeading(true) end +--- Get true (or magnetic) heading of carrier into the wind. This accounts for the angled runway. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned. +-- @return #number Carrier heading in degrees. +function AIRBOSS:GetHeadingIntoWind(magnetic) + + -- Get direction the wind is blowing from. This is where we want to go. + local windfrom=self:GetCoordinate():GetWind(50) + + -- Actually, we want the runway in the wind. + local intowind=windfrom-self.carrierparam.rwyangle + + -- Magnetic heading. + if magnetic then + intowind=intowind-self.magvar + end + + -- Adjust negative values. + if intowind<0 then + intowind=intowind+360 + end + + return intowind +end + +--- Get base recovery course (BRC) when the carrier would head into the wind. +-- This includes the current wind direction and accounts for the angled runway. +-- @param #AIRBOSS self +-- @return #number BRC into the wind in degrees. +function AIRBOSS:GetBRCintoWind() + -- BRC is the magnetic heading. + return self:GetHeadingIntoWind(true) +end + --- Get final bearing (FB) of carrier. -- By default, the routine returns the magnetic FB depending on the current map (Caucasus, NTTR, Normandy, Persion Gulf etc). @@ -9800,6 +9746,543 @@ function AIRBOSS:_StepHint(playerData, step) end end +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- CARRIER ROUTING Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check for possible collisions between two coordinates. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE coordto Coordinate to which the collision is check. +-- @param Core.Point#COORDINATE coordfrom Coordinate from which the collision is check. +-- @return #boolean If true, surface type ahead is not deep water. +-- @return #number Max free distance in meters. +function AIRBOSS:_CheckCollisionCoord(coordto, coordfrom) + + -- Increment in meters. + local dx=100 + + -- From coordinate. Default 500 in front of the carrier. + local d=0 + if coordfrom then + d=0 + else + d=250 + coordfrom=self:GetCoordinate():Translate(d, self:GetHeading()) + end + + -- Distance between the two coordinates. + local dmax=coordfrom:Get2DDistance(coordto) + + -- Direction. + local direction=coordfrom:HeadingTo(coordto) + + -- Scan path between the two coordinates. + local clear=true + while d<=dmax do + + -- Check point. + local cp=coordfrom:Translate(d, direction) + + -- Check if surface type is water. + if not cp:IsSurfaceTypeWater() then + + -- Debug mark points. + if self.Debug then + local st=cp:GetSurfaceType() + cp:MarkToAll(string.format("Collision check surface type %d", st)) + end + + -- Collision WARNING! + clear=false + break + end + + -- Increase distance. + d=d+dx + end + + local text="" + if clear then + text=string.format("Path into direction %03d° is clear for the next %.1f NM.", direction, UTILS.MetersToNM(d)) + else + text=string.format("Detected obstacle at distance %.1f NM into direction %03d°.", UTILS.MetersToNM(d), direction) + end + self:T2(self.lid..text) + + return not clear, d +end + + +--- Check Collision. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE fromcoord Coordinate from which the path to the next WP is calculated. Default current carrier position. +-- @return #boolean If true, surface type ahead is not deep water. +function AIRBOSS:_CheckFreePathToNextWP(fromcoord) + + -- Position. + fromcoord=fromcoord or self:GetCoordinate():Translate(250, self:GetHeading()) + + -- Next wp = current+1 (or last) + local Nnextwp=math.min(self.currentwp+1, #self.waypoints) + + -- Next waypoint. + local nextwp=self.waypoints[Nnextwp] --Core.Point#COORDINATE + + -- Check for collision. + local collision=self:_CheckCollisionCoord(nextwp, fromcoord) + + return collision +end + +--- Find free path to the next waypoint. +-- @param #AIRBOSS self +function AIRBOSS:_Pathfinder() + + -- Heading and current coordiante. + local hdg=self:GetHeading() + local cv=self:GetCoordinate() + + -- Possible directions. + local directions={-20, 20, -30, 30, -40, 40, -50, 50, -60, 60, -70, 70, -80, 80, -90, 90, -100, 100} + + -- Starboard turns up to 90 degrees. + for _,_direction in pairs(directions) do + + -- New direction. + local direction=hdg+_direction + + -- Check for collisions in the next 20 NM of the current direction. + local _, dfree=self:_CheckCollisionCoord(cv:Translate(UTILS.NMToMeters(20), direction), cv) + + -- Loop over distances and find the first one which gives a clear path to the next waypoint. + local distance=500 + while distance<=dfree do + + -- Coordinate from which we calculate the path. + local fromcoord=cv:Translate(distance, direction) + + -- Check for collision between point and next waypoint. + local collision=self:_CheckFreePathToNextWP(fromcoord) + + -- Debug info. + self:T2(self.lid..string.format("Pathfinder d=%.1f m, direction=%03d°, collision=%s", distance, direction, tostring(collision))) + + -- If path is clear, we start a little detour. + if not collision then + self:CarrierDetour(fromcoord) + return + end + + distance=distance+500 + end + end +end + + +--- Carrier resumes the route at its next waypoint. +--@param #AIRBOSS self +--@param Core.Point#COORDINATE goto (Optional) First goto this coordinate before resuming route. +--@return #AIRBOSS self +function AIRBOSS:CarrierResumeRoute(goto) + + -- Make carrier resume its route. + AIRBOSS._ResumeRoute(self.carrier:GetGroup(), self, goto) + + return self +end + + +--- Let the carrier make a detour to a given point. When it reaches the point, it will resume its normal route. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE coord Coordinate of the detour. +-- @param #number speed Speed in knots. Default is current carrier velocity. +-- @param #boolean uturn (Optional) If true, carrier will go back to where it came from before it resumes its route to the next waypoint. +-- @return #AIRBOSS self +function AIRBOSS:CarrierDetour(coord, speed, uturn) + + -- Current coordinate of the carrier. + local pos0=self:GetCoordinate() + + -- Speed in km/h. + local speedkmh=UTILS.KnotsToKmph(speed or self.carrier:GetVelocityKNOTS()) + + -- Waypoint table. + local wp={} + + -- Create from/to waypoints. + table.insert(wp, pos0:WaypointGround(speedkmh)) + table.insert(wp, coord:WaypointGround(speedkmh)) + + -- If enabled, go back to where you came from. + if uturn then + table.insert(wp, pos0:WaypointGround(speedkmh)) + end + + -- Get carrier group. + local group=self.carrier:GetGroup() + + -- Passing waypoint taskfunction + local TaskResumeRoute=group:TaskFunction("AIRBOSS._ResumeRoute", self) + + -- Set task to restart route at the last point. + group:SetTaskWaypoint(wp[#wp], TaskResumeRoute) + + -- Debug mark. + if self.Debug then + coord:MarkToAll("Detour Point") + end + + -- Detour switch true. + self.detour=true + + -- Route carrier into the wind. + self.carrier:Route(wp) +end + +--- Let the carrier turn into the wind. +-- @param #AIRBOSS self +-- @param #number distance Distance in NM from current position. +-- @param #number speed Speed in knots. Default 15 knots. +-- @return #AIRBOSS self +function AIRBOSS:CarrierTurnIntoWind(distance, speed) + + -- Time in seconds. + local time=distance/speed*3600 + + -- Debug message. + self:T(self.lid..string.format("Carrier steaming into the wind. Distance=%.1f NM, Speed=%.1f knots, Time=%d sec.", distance, speed, time)) + + -- Get heading into the wind accounting for angled runway. + local intowind=self:GetHeadingIntoWind(false) + + -- Length of path into the wind in meters. + local dist=UTILS.NMToMeters(distance) + + -- Translate current position. + local pos1=self:GetCoordinate():Translate(dist, intowind) + + -- Debug mark. + if self.Debug then + pos1:MarkToAll("Into the wind point") + end + + -- Speed in km/h. + local speedkmh=UTILS.KnotsToKmph(speed) + + -- Return to coordinate if collision is detected. + self.Creturnto=self:GetCoordinate() + + -- Let the carrier make a detour from its route but return to its current position. + self:CarrierDetour(pos1, speed, true) + + -- Set switch that we are currently turning into the wind. + self.turnintowind=true + + return self +end + +--- Patrol carrier. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:_PatrolRoute() + + -- Get carrier group. + local CarrierGroup=self.carrier:GetGroup() + + -- Waypoints of group. + local Waypoints = CarrierGroup:GetTemplateRoutePoints() + + -- Loop over waypoints. + for n=1,#Waypoints do + + -- Passing waypoint taskfunction + local TaskPassingWP=CarrierGroup:TaskFunction("AIRBOSS._PassingWaypoint", self, n, #Waypoints) + + -- Call task function when carrier arrives at waypoint. + CarrierGroup:SetTaskWaypoint(Waypoints[n], TaskPassingWP) + end + + -- Set waypoint table. + local i=1 + for _,point in ipairs(Waypoints) do + + -- Coordinate of the waypoint + local coord=COORDINATE:New(point.x, point.alt, point.y) + + -- Set velocity of the coordinate. + coord:SetVelocity(point.speed) + + -- Add to table. + table.insert(self.waypoints, coord) + + -- Debug info. + if self.Debug then + coord:MarkToAll(string.format("Carrier Waypoint %d, Speed=%.1f knots", i, UTILS.MpsToKnots(point.speed))) + end + + -- Increase counter. + i=i+1 + end + + -- Current waypoint is 1. + self.currentwp=1 + + -- Route carrier group. + CarrierGroup:Route(Waypoints) + + return self +end + +--- Estimated the carrier position at some point in the future given the current waypoints and speeds. +-- @param #AIRBOSS self +-- @return DCS#time ETA abs. time in seconds. +function AIRBOSS:_GetETAatNextWP() + + -- Current waypoint + local cwp=self.currentwp + + -- Current abs. time. + local tnow=timer.getAbsTime() + + -- Current position. + local p=self:GetCoordinate() + + -- Current velocity [m/s]. + local v=self.carrier:GetVelocityMPS() + + -- Distance to next waypoint. + local s=0 + if #self.waypoints>cwp then + s=p:Get2DDistance(self.waypoints[cwp+1]) + end + + -- v=s/t <==> t=s/v + local t=s/v + + -- ETA + local eta=t+tnow + + return eta +end + + +--- Check if heading or position of carrier have changed significantly. +-- @param #AIRBOSS self +function AIRBOSS:_CheckPatternUpdate() + + -- TODO: Make parameters input values. + + -- Min 10 min between pattern updates. + local dTPupdate=10*60 + + -- Update if carrier moves by more than 2.5 NM. + local Dupdate=UTILS.NMToMeters(2.5) + + -- Update if carrier turned by more than 5 degrees. + local Hupdate=5 + + -- Time since last pattern update + local dt=timer.getTime()-self.Tpupdate + + -- At least 10 min between updates. Not yet... + if dt=1 + + -- No update if carrier is turning! + if turning then + self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) + return + end + + -- Check if orientation changed. + local Hchange=false + if math.abs(deltaHeading)>=Hupdate then + self:T(self.lid..string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) + Hchange=true + end + + -- Get distance to saved position. + local dist=pos:Get2DDistance(self.Cposition) + + -- Check if carrier moved more than ~10 km. + local Dchange=false + if dist>=Dupdate then + self:T(self.lid..string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) + Dchange=true + end + + -- If heading or distance changed ==> update marshal AI patterns. + if Hchange or Dchange then + + -- Loop over all marshal flights + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Update marshal pattern of AI keeping the same stack. + if flight.ai then + self:_MarshalAI(flight, flight.flag:Get()) + end + + end + + -- Inform player about new final bearing. + if Hchange then + -- 99, new final bearing XXX + local FB=self:GetFinalBearing(true) + local text=string.format("new final bearing %03d°.", FB) + self:MessageToMarshal(text, "AIRBOSS", "99", 10) + end + + -- Reset parameters for next update check. + self.Corientation=vNew + self.Cposition=pos + self.Tpupdate=timer.getTime() + end + +end + +--- Function called when a group is passing a waypoint. +--@param Wrapper.Group#GROUP group Group that passed the waypoint +--@param #AIRBOSS airboss Airboss object. +--@param #number i Waypoint number that has been reached. +--@param #number final Final waypoint number. +function AIRBOSS._PassingWaypoint(group, airboss, i, final) + + -- Debug message. + local text=string.format("Group %s passing waypoint %d of %d.", group:GetName(), i, final) + + -- Debug smoke and marker. + if airboss.Debug and false then + local pos=group:GetCoordinate() + pos:SmokeRed() + local MarkerID=pos:MarkToAll(string.format("Group %s reached waypoint %d", group:GetName(), i)) + end + + -- Debug message. + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Set current waypoint. + airboss.currentwp=i + + -- If final waypoint reached, do route all over again. + if i==final and final>1 and airboss.adinfinitum then + airboss:_PatrolRoute() + end +end + +--- Carrier Strike Group resumes the route of the waypoints defined in the mission editor. +--@param Wrapper.Group#GROUP group Carrier Strike Group that passed the waypoint. +--@param #AIRBOSS airboss Airboss object. +--@param Core.Point#COORDINATE goto Go to coordinate before route is resumed. +function AIRBOSS._ResumeRoute(group, airboss, goto) + + -- Next wp = current+1 (or last) + local nextwp=math.min(airboss.currentwp+1, #airboss.waypoints) + + -- Debug message. + local text=string.format("Group %s is resuming route. Next waypoint %d.", group:GetName(), nextwp) + + -- Debug message. + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Waypoints array. + local waypoints={} + + -- Get current velocity in km/h. + local velocity=group:GetVelocityKMH() + + -- Current positon as first waypoint. + local wp0=group:GetCoordinate():WaypointGround(velocity) + table.insert(waypoints, wp0) + + if goto then + local wp1=goto:WaypointGround(velocity) + table.insert(waypoints, wp1) + end + + -- Loop over all remaining waypoints. + for i=nextwp, #airboss.waypoints do + + -- Coordinate of the next WP. + local coord=airboss.waypoints[i] --Core.Point#COORDINATE + + -- Speed in km/h of that WP. Velocity is in m/s. + local speed=coord.Velocity*3.6 + + -- Create waypoint. + local wp=coord:WaypointGround(speed) + + -- Passing waypoint task function. + local TaskPassingWP=group:TaskFunction("AIRBOSS._PassingWaypoint", airboss, i, #airboss.waypoints) + + -- Call task function when carrier arrives at waypoint. + group:SetTaskWaypoint(wp, TaskPassingWP) + + -- Add waypoints to table. + table.insert(waypoints, wp) + end + + -- Set turn into wind switch false. + airboss.turnintowind=false + airboss.detour=false + + -- Route group. + group:Route(waypoints) +end + +--- Function called when a group has reached the holding zone. +--@param Wrapper.Group#GROUP group Group that reached the holding zone. +--@param #AIRBOSS airboss Airboss object. +--@param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone. +function AIRBOSS._ReachedHoldingZone(group, airboss, flight) + + -- Debug message. + local text=string.format("Flight %s reached holding zone.", group:GetName()) + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Debug mark. + if airboss.Debug then + group:GetCoordinate():MarkToAll(text) + end + + -- Set holding flag true and set timestamp for marshal time check. + if flight then + flight.holding=true + flight.time=timer.getAbsTime() + end +end + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- MISC functions ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -9960,7 +10443,6 @@ end function AIRBOSS:_GetAngels(alt) if alt then - --local angels=math.floor(UTILS.MetersToFeet(alt)/1000) local angels=UTILS.Round(UTILS.MetersToFeet(alt)/1000, 0) return angels else @@ -10856,9 +11338,13 @@ function AIRBOSS:_AddF10Commands(_unitName) end -- F10/Airboss/ - local _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) - --AIRBOSS.MenuRoot[gid]=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) - --local _rootPath=AIRBOSS.MenuRoot[gid] + local _rootPath + if self.menusingle then + _rootPath=AIRBOSS.MenuF10[gid] + else + _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) + end + -------------------------------- -- F10/Airboss//F1 Help From ff0371faa5aa1bd14d4d560f4daef4cdcdd69815 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 27 Jan 2019 13:07:48 +0100 Subject: [PATCH 140/147] CONTROLLABLE Fixed task bombing runway. --- Moose Development/Moose/Ops/Airboss.lua | 14 +++---- .../Moose/Wrapper/Controllable.lua | 37 +++++++++++++------ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 5925d9b40..81d772db2 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -9881,12 +9881,12 @@ end --- Carrier resumes the route at its next waypoint. --@param #AIRBOSS self ---@param Core.Point#COORDINATE goto (Optional) First goto this coordinate before resuming route. +--@param Core.Point#COORDINATE gotocoord (Optional) First goto this coordinate before resuming route. --@return #AIRBOSS self -function AIRBOSS:CarrierResumeRoute(goto) +function AIRBOSS:CarrierResumeRoute(gotocoord) -- Make carrier resume its route. - AIRBOSS._ResumeRoute(self.carrier:GetGroup(), self, goto) + AIRBOSS._ResumeRoute(self.carrier:GetGroup(), self, gotocoord) return self end @@ -10202,8 +10202,8 @@ end --- Carrier Strike Group resumes the route of the waypoints defined in the mission editor. --@param Wrapper.Group#GROUP group Carrier Strike Group that passed the waypoint. --@param #AIRBOSS airboss Airboss object. ---@param Core.Point#COORDINATE goto Go to coordinate before route is resumed. -function AIRBOSS._ResumeRoute(group, airboss, goto) +--@param Core.Point#COORDINATE gotocoord Go to coordinate before route is resumed. +function AIRBOSS._ResumeRoute(group, airboss, gotocoord) -- Next wp = current+1 (or last) local nextwp=math.min(airboss.currentwp+1, #airboss.waypoints) @@ -10225,8 +10225,8 @@ function AIRBOSS._ResumeRoute(group, airboss, goto) local wp0=group:GetCoordinate():WaypointGround(velocity) table.insert(waypoints, wp0) - if goto then - local wp1=goto:WaypointGround(velocity) + if gotocoord then + local wp1=gotocoord:WaypointGround(velocity) table.insert(waypoints, wp1) end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 0353c42f9..eac3ce207 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1068,17 +1068,27 @@ end ---- (AIR) Delivering weapon on the runway. +--- (AIR) Delivering weapon on the runway. See [hoggit](https://wiki.hoggitworld.com/view/DCS_task_bombingRunway) +-- +-- Make sure the aircraft has the following role: +-- +-- * CAS +-- * Ground Attack +-- * Runway Attack +-- * Anti-Ship Strike +-- * AFAC +-- * Pinpoint Strike +-- -- @param #CONTROLLABLE self -- @param Wrapper.Airbase#AIRBASE Airbase Airbase to attack. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. See [DCS enum weapon flag](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag). Default 2147485694 = AnyBomb (GuidedBomb + AnyUnguidedBomb). +-- @param DCS#AI.Task.WeaponExpend WeaponExpend Enum AI.Task.WeaponExpend that defines how much munitions the AI will expend per attack run. Default "ALL". +-- @param #number AttackQty Number of times the group will attack if the target. Default 1. -- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. +-- @param #boolean GroupAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a group and not to a single aircraft. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskBombingRunway( Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack } ) +function CONTROLLABLE:TaskBombingRunway(Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack) + self:F2( { self.ControllableName, Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack } ) -- BombingRunway = { -- id = 'BombingRunway', @@ -1088,19 +1098,24 @@ function CONTROLLABLE:TaskBombingRunway( Airbase, WeaponType, WeaponExpend, Atta -- expend = enum AI.Task.WeaponExpend, -- attackQty = number, -- direction = Azimuth, --- controllableAttack = boolean, +-- groupAttack = boolean, -- } --- } +-- } + + -- Defaults. + WeaponType=WeaponType or 2147485694 + WeaponExpend=WeaponExpend or AI.Task.WeaponExpend.ALL + AttackQty=AttackQty or 1 local DCSTask DCSTask = { id = 'BombingRunway', params = { - point = Airbase:GetID(), + runwayId = Airbase:GetID(), weaponType = WeaponType, expend = WeaponExpend, attackQty = AttackQty, direction = Direction, - controllableAttack = ControllableAttack, + groupAttack = GroupAttack, }, }, From 77bee89ea580d92cb380f791219c66584c42154c Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 27 Jan 2019 17:19:41 +0100 Subject: [PATCH 141/147] Updates --- Moose Development/Moose/AI/AI_A2G_BAI.lua | 36 ++--- Moose Development/Moose/AI/AI_A2G_CAS.lua | 32 ++-- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 141 ++++++++---------- Moose Development/Moose/AI/AI_Air.lua | 2 +- Moose Development/Moose/Core/Set.lua | 48 ++++++ .../Moose/Wrapper/Controllable.lua | 4 +- 6 files changed, 143 insertions(+), 120 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index e761e0b70..88f9bf491 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -74,21 +74,22 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit if DefenderGroup:IsAlive() then + local EngageAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + -- Determine the distance to the target. -- If it is less than 10km, then attack without a route. -- Otherwise perform a route attack. local DefenderCoord = DefenderGroup:GetPointVec3() - DefenderCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() - TargetCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) local EngageRoute = {} - - local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) --- Calculate the target route point. @@ -96,39 +97,37 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, + EngageSpeed, true ) EngageRoute[#EngageRoute+1] = FromWP - local ToCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - self:SetTargetDistance( ToCoord ) -- For RTB status check + self:SetTargetDistance( TargetCoord ) -- For RTB status check - local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) + local FromEngageAngle = TargetCoord:GetAngleDegrees( TargetCoord:GetDirectionVec3( DefenderCoord ) ) local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) --- Create a route point of type air. - local ToWP = ToCoord:Translate( EngageDistance, FromEngageAngle ):WaypointAir( + local ToWP = TargetCoord:Translate( EngageDistance, FromEngageAngle ):WaypointAir( self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, + EngageSpeed, true ) - self:F( { Angle = FromEngageAngle, ToTargetSpeed = ToTargetSpeed } ) - self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) - EngageRoute[#EngageRoute+1] = ToWP local AttackTasks = {} - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:T( { "Engage Unit evaluation:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) - self:T( { "Eliminating Unit:", AttackUnit:GetName() } ) - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) + for OrderedID, AttackUnit in ipairs( self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "BAI Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) + break + end end end @@ -139,6 +138,7 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit else DefenderGroup:OptionROEOpenFire() DefenderGroup:OptionROTEvadeFire() + DefenderGroup:OptionKeepWeaponsOnThreat() AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 6ed78a5eb..58069c5b6 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -74,21 +74,18 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit if DefenderGroup:IsAlive() then - -- Determine the distance to the target. - -- If it is less than 10km, then attack without a route. - -- Otherwise perform a route attack. + local EngageAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) local DefenderCoord = DefenderGroup:GetPointVec3() - DefenderCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() - TargetCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) local EngageRoute = {} - - local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) --- Calculate the target route point. @@ -96,7 +93,7 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, + EngageSpeed, true ) @@ -108,25 +105,25 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) --- Create a route point of type air. - local ToWP = TargetCoord:Translate( EngageDistance, FromEngageAngle ):WaypointAir( + local ToWP = TargetCoord:Translate( EngageDistance, FromEngageAngle, true ):WaypointAir( self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, + EngageSpeed, true ) - self:F( { Angle = FromEngageAngle, ToTargetSpeed = ToTargetSpeed } ) - self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) - EngageRoute[#EngageRoute+1] = ToWP local AttackTasks = {} - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:T( { "Eliminating Unit:", AttackUnit:GetName() } ) - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) + for OrderedID, AttackUnit in ipairs( self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:F( { "CAS Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) + break + end end end @@ -137,6 +134,7 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit else DefenderGroup:OptionROEOpenFire() DefenderGroup:OptionROTEvadeFire() + DefenderGroup:OptionKeepWeaponsOnThreat() AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index 72d22577c..c459918be 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -129,103 +129,80 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni -- If it is less than 50km, then attack without a route. -- Otherwise perform a route attack. + local EngageAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + local DefenderCoord = DefenderGroup:GetPointVec3() - DefenderCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() - TargetCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) --- if TargetDistance >= 50000 then - - local EngageRoute = {} - - local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - - --- Calculate the target route point. - - local FromWP = DefenderCoord:WaypointAir( - self.PatrolAltType or "RADIO", - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - EngageRoute[#EngageRoute+1] = FromWP - - local ToCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - self:SetTargetDistance( ToCoord ) -- For RTB status check - - local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) - local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 25000 ) + local EngageRoute = {} - --- Create a route point of type air, 50km from the center of the attack point. - local ToWP = ToCoord:Translate( EngageDistance, FromEngageAngle ):WaypointAir( - self.PatrolAltType or "RADIO", - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = FromEngageAngle, ToTargetSpeed = ToTargetSpeed } ) - self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToWP - - local AttackTasks = {} - - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromWP + + self:SetTargetDistance( TargetCoord ) -- For RTB status check + + local FromEngageAngle = TargetCoord:GetAngleDegrees( TargetCoord:GetDirectionVec3( DefenderCoord ) ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 25000 ) + + --- Create a route point of type air, 50km from the center of the attack point. + + local ToWP = TargetCoord:Translate( EngageDistance, FromEngageAngle, true ):WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = ToWP + + local AttackTasks = {} + + for OrderedID, AttackUnit in ipairs( self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) ) do + if AttackUnit then if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:T( { "Engage Unit evaluation:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) local HasRadar = AttackUnit:HasSEAD() if HasRadar then - self:T( { "Eliminating Unit:", AttackUnit:GetName() } ) - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) + self:F( { "SEAD Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) + break end end end - - if #AttackTasks == 0 then - self:E( DefenderGroupName .. ": No targets found -> Going RTB") - self:Return() - self:__RTB( self.TaskDelay ) - else - DefenderGroup:OptionROEOpenFire() - DefenderGroup:OptionROTVertical() - DefenderGroup:OptionKeepWeaponsOnThreat() - --DefenderGroup:OptionRTBAmmo( Weapon.flag.AnyASM ) - - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) - EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) - end + end - DefenderGroup:Route( EngageRoute, self.TaskDelay ) + if #AttackTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( self.TaskDelay ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTVertical() + DefenderGroup:OptionKeepWeaponsOnThreat() + --DefenderGroup:OptionRTBAmmo( Weapon.flag.AnyASM ) + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + end + + DefenderGroup:Route( EngageRoute, self.TaskDelay ) --- else --- local AttackTasks = {} --- --local AttackUnit = self.AttackSetUnit:GetRandom() -- Wrapper.Unit#UNIT --- for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do --- if AttackUnit:IsAlive() and AttackUnit:IsGround() then --- local HasRadar = AttackUnit:HasSEAD() --- if HasRadar then --- self:T( { "Eliminating Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) --- AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) --- AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) --- end --- end --- end --- local DefenderTask = DefenderGroup:TaskCombo( AttackTasks ) --- --- DefenderGroup:OptionROEOpenFire() --- DefenderGroup:OptionROTVertical() --- DefenderGroup:OptionKeepWeaponsOnThreat() --- DefenderGroup:OptionRTBAmmo( Weapon.flag.AnyASM ) --- --- DefenderGroup:SetTask( DefenderTask, 0 ) --- end end else self:E( DefenderGroupName .. ": No targets found -> Going RTB") diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 149834022..018d18839 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -615,7 +615,7 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) EngageRoute[#EngageRoute+1] = ToRTBRoutePoint local Tasks = {} - Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_A2G_ENGAGE.RTBRoute", self ) + Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_AIR.RTBRoute", self ) EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 6acb294ab..e4e2c3edf 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -2033,6 +2033,54 @@ do -- SET_UNIT return self end + + --- Get the SET of the SET_UNIT **sorted per Threat Level**. + -- + -- @param #SET_UNIT self + -- @param #number FromThreatLevel The TreatLevel to start the evaluation **From** (this must be a value between 0 and 10). + -- @param #number ToThreatLevel The TreatLevel to stop the evaluation **To** (this must be a value between 0 and 10). + -- @return #SET_UNIT self + -- @usage + -- + -- + function SET_UNIT:GetSetPerThreatLevel( FromThreatLevel, ToThreatLevel ) + self:F2( arg ) + + local ThreatLevelSet = {} + + if self:Count() ~= 0 then + for UnitName, UnitObject in pairs( self.Set ) do + local Unit = UnitObject -- Wrapper.Unit#UNIT + + local ThreatLevel = Unit:GetThreatLevel() + ThreatLevelSet[ThreatLevel] = ThreatLevelSet[ThreatLevel] or {} + ThreatLevelSet[ThreatLevel].Set = ThreatLevelSet[ThreatLevel].Set or {} + ThreatLevelSet[ThreatLevel].Set[UnitName] = UnitObject + self:F( { ThreatLevel = ThreatLevel, ThreatLevelSet = ThreatLevelSet[ThreatLevel].Set } ) + end + + + local OrderedPerThreatLevelSet = {} + + local ThreatLevelIncrement = FromThreatLevel <= ToThreatLevel and 1 or -1 + + + for ThreatLevel = FromThreatLevel, ToThreatLevel, ThreatLevelIncrement do + self:F( { ThreatLevel = ThreatLevel } ) + local ThreatLevelItem = ThreatLevelSet[ThreatLevel] + if ThreatLevelItem then + for UnitName, UnitObject in pairs( ThreatLevelItem.Set ) do + table.insert( OrderedPerThreatLevelSet, UnitObject ) + end + end + end + + return OrderedPerThreatLevelSet + end + + end + + --- Iterate the SET_UNIT **sorted *per Threat Level** and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. -- -- @param #SET_UNIT self diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 96455d007..f85edcfad 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -400,7 +400,7 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) local Controller = self:_GetController() --self:I( "Before SetTask" ) Controller:setTask( DCSTask ) - self:F( { DCSTask = DCSTask } ) + self:I( { ControllableName = self:GetName(), DCSTask = DCSTask } ) else BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) end @@ -408,7 +408,7 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) if not WaitTime or WaitTime == 0 then SetTask( self, DCSTask ) - self:F( { DCSTask = DCSTask } ) + self:I( { ControllableName = self:GetName(), DCSTask = DCSTask } ) else self.TaskScheduler:Schedule( self, SetTask, { DCSTask }, WaitTime ) end From 9e450ce76b0c5d0d3eddd771d3ac5047b4402135 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 28 Jan 2019 22:03:21 +0100 Subject: [PATCH 142/147] RECOVERYTANKER v1.0.4 - Fixed PG airbase enumerator names - Rescuehelo v1.0.3 - minor stuff --- Moose Development/Moose/AI/AI_A2A.lua | 2 +- Moose Development/Moose/Core/Point.lua | 13 ++- Moose Development/Moose/Functional/RAT.lua | 9 +- Moose Development/Moose/Ops/Airboss.lua | 2 +- .../Moose/Ops/RecoveryTanker.lua | 37 +++--- Moose Development/Moose/Ops/RescueHelo.lua | 110 +++++++++++++----- Moose Development/Moose/Wrapper/Airbase.lua | 66 +++++------ Moose Development/Moose/Wrapper/Group.lua | 23 ---- .../Moose/Wrapper/Positionable.lua | 21 ++++ 9 files changed, 170 insertions(+), 113 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A.lua b/Moose Development/Moose/AI/AI_A2A.lua index 14c137dc2..2b3269b74 100644 --- a/Moose Development/Moose/AI/AI_A2A.lua +++ b/Moose Development/Moose/AI/AI_A2A.lua @@ -7,7 +7,7 @@ -- === -- -- @module AI.AI_A2A --- @image AI_Air_To_Air_Dispatching.JPG +-- @image MOOSE.JPG --BASE:TraceClass("AI_A2A") diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index b5cd1ce26..2a5c8d0ee 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1062,8 +1062,8 @@ do -- COORDINATE RoutePoint.speed_locked = SpeedLocked -- ETA. - RoutePoint.ETA=nil - RoutePoint.ETA_locked = false + RoutePoint.ETA=0 + RoutePoint.ETA_locked=true -- Waypoint description. RoutePoint.name=description @@ -1081,7 +1081,7 @@ do -- COORDINATE self:T("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!") end - --self:MarkToAll(string.format("Landing waypoint at airbase %s", airbase:GetName())) + --self:MarkToAll(string.format("Landing waypoint at airbase %s, ID=%d, Category=%d", airbase:GetName(), AirbaseID, AirbaseCategory )) end -- Waypoint tasks. @@ -1089,6 +1089,11 @@ do -- COORDINATE RoutePoint.task.id = "ComboTask" RoutePoint.task.params = {} RoutePoint.task.params.tasks = DCSTasks or {} + + --RoutePoint.properties={} + --RoutePoint.properties.addopt={} + + --RoutePoint.formation_template="" -- Debug. self:T({RoutePoint=RoutePoint}) @@ -1165,7 +1170,7 @@ do -- COORDINATE -- HeliGroup:Route( { LandWaypoint }, 1 ) -- Start landing the helicopter in one second. -- function COORDINATE:WaypointAirLanding( Speed, airbase, DCSTasks, description ) - return self:WaypointAir(nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, nil, airbase, DCSTasks, description) + return self:WaypointAir(nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, false, airbase, DCSTasks, description) end diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 60410bf24..dc5563546 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -717,6 +717,11 @@ function RAT:Spawn(naircraft) self.FLcruise=005*RAT.unit.FL2m end end + + -- Enable helos to go to destinations 100 meters away. + if self.category==RAT.cat.heli then + self.mindist=50 + end -- Run consistency checks. self:_CheckConsistency() @@ -1812,14 +1817,14 @@ function RAT:ATC_Delay(time) end --- Set minimum distance between departure and destination. Default is 5 km. --- Minimum distance should not be smaller than maybe ~500 meters to ensure that departure and destination are different. +-- Minimum distance should not be smaller than maybe ~100 meters to ensure that departure and destination are different. -- @param #RAT self -- @param #number dist Distance in km. -- @return #RAT RAT self object. function RAT:SetMinDistance(dist) self:F2(dist) -- Distance in meters. Absolute minimum is 500 m. - self.mindist=math.max(500, dist*1000) + self.mindist=math.max(100, dist*1000) return self end diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 81d772db2..4fe125986 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -71,7 +71,7 @@ -- Bankler was kind enough to allow me to add this to the class - thanks again! -- -- @module Ops.Airboss --- @image MOOSE.JPG +-- @image Ops_Airboss.png --- AIRBOSS class. -- @type AIRBOSS diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 398745c7f..e396691dd 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -9,7 +9,8 @@ -- * Automatic respawning when tanker runs out of fuel for 24/7 operations. -- * Tanker can be spawned cold or hot on the carrier or at any other airbase or directly in air. -- * Automatic AA TACAN beacon setting. --- * Multiple tankers at different carriers due to object oriented approach. +-- * Multiple tankers at the same carrier. +-- * Multiple carriers due to object oriented approach. -- * Finite State Machine (FSM) implementation, which allows the mission designer to hook into certain events. -- -- === @@ -18,7 +19,7 @@ -- ### Special thanks to **HighwaymanEd** for testing and suggesting improvements! -- -- @module Ops.RecoveryTanker --- @image MOOSE.JPG +-- @image Ops_RecoveryTanker.png --- RECOVERYTANKER class. -- @type RECOVERYTANKER @@ -54,6 +55,7 @@ -- @field DCS#Vec3 orientlast Orientation of the carrier for checking if carrier is currently turning. -- @field Core.Point#COORDINATE position Position of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. -- @field #string alias Alias of the spawn group. +-- @field #number uid Unique ID of this tanker. -- @extends Core.Fsm#FSM --- Recovery Tanker. @@ -252,15 +254,16 @@ RECOVERYTANKER = { orientlast = nil, position = nil, alias = nil, + uid = 0, } --- Unique ID (global). --- @field #number uid Unique ID (global). -RECOVERYTANKER.uid=0 +-- @field #number UID Unique ID (global). +RECOVERYTANKER.UID=0 --- Class version. -- @field #string version -RECOVERYTANKER.version="1.0.3" +RECOVERYTANKER.version="1.0.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -305,14 +308,17 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- Tanker group name. self.tankergroupname=tankergroupname - -- Save self in static object. Easier to retrieve later. - self.carrier:SetState(self.carrier, "RECOVERYTANKER", self) - -- Increase unique ID. - RECOVERYTANKER.uid=RECOVERYTANKER.uid+1 + RECOVERYTANKER.UID=RECOVERYTANKER.UID+1 + + -- Unique ID of this tanker. + self.uid=RECOVERYTANKER.UID + + -- Save self in static object. Easier to retrieve later. + self.carrier:SetState(self.carrier, string.format("RECOVERYTANKER_%d", self.uid) , self) -- Set unique spawn alias. - self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.tankergroupname, RECOVERYTANKER.uid) + self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.tankergroupname, RECOVERYTANKER.UID) -- Log ID. self.lid=string.format("RECOVERYTANKER %s |", self.alias) @@ -931,7 +937,6 @@ end -- @param #string Event Event. -- @param #string To To state. function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) - -- Debug message. local text=string.format("Updating recovery tanker %s racetrack pattern.", self.tanker:GetName()) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) @@ -981,6 +986,7 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) -- Set update time. self.Tupdate=timer.getTime() + end --- On after "RTB" event. Send tanker back to carrier. @@ -1149,9 +1155,9 @@ function RECOVERYTANKER:_InitPatternTaskFunction() -- Task script. local DCSScript = {} - DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername) -- The carrier unit that holds the self object. - DCSScript[#DCSScript+1] = string.format('local mytanker = mycarrier:GetState(mycarrier, \"RECOVERYTANKER\") ') -- Get the RECOVERYTANKER self object. - DCSScript[#DCSScript+1] = string.format('mytanker:PatternUpdate()') -- Call the function, e.g. mytanker.(self) + DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername) -- The carrier unit that holds the self object. + DCSScript[#DCSScript+1] = string.format('local mytanker = mycarrier:GetState(mycarrier, \"RECOVERYTANKER_%d\") ', self.uid) -- Get the RECOVERYTANKER self object. + DCSScript[#DCSScript+1] = string.format('mytanker:PatternUpdate()') -- Call the function, e.g. mytanker.(self) -- Create task. local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) @@ -1159,7 +1165,6 @@ function RECOVERYTANKER:_InitPatternTaskFunction() return DCSTask end - --- Init waypoint after spawn. Tanker is first guided to a position astern the carrier and starts its racetrack pattern from there. -- @param #RECOVERYTANKER self -- @param #number dist Distance [NM] of initial waypoint astern carrier. Default 8 NM. @@ -1195,7 +1200,7 @@ function RECOVERYTANKER:_InitRoute(dist, delay) end -- Task to update pattern when wp 2 is reached. - local task=self:_InitPatternTaskFunction() + local task=self:_InitPatternTaskFunction() -- Waypoints. local wp={} diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 0142bb201..6f38337ca 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -17,7 +17,7 @@ -- ### Contributions: Flightcontrol (@{AI.AI_Formation} class being used here) -- -- @module Ops.RescueHelo --- @image MOOSE.JPG +-- @image Ops_RescueHelo.png --- RESCUEHELO class. -- @type RESCUEHELO @@ -49,6 +49,7 @@ -- @field #boolean rtb If true, Helo will be return to base on the next status check. -- @field #number hid Unit ID of the helo group. (Global) Running number. -- @field #string alias Alias of the spawn group. +-- @field #number uid Unique ID of this helo. -- @extends Core.Fsm#FSM --- Rescue Helo @@ -217,22 +218,23 @@ RESCUEHELO = { rtb = nil, carrierstop = nil, alias = nil, + uid = 0, } --- Unique ID (global). -- @field #number uid Unique ID (global). -RESCUEHELO.uid=0 +RESCUEHELO.UID=0 --- Class version. -- @field #string version -RESCUEHELO.version="1.0.2" +RESCUEHELO.version="1.0.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Add messages for rescue mission. --- TODO: Add option to stop carrier while rescue operation is in progress? Done but NOT working! +-- NOPE: Add messages for rescue mission. +-- NOPE: Add option to stop carrier while rescue operation is in progress? Done but NOT working. Postponed... -- DONE: Write documentation. -- DONE: Add option to deactivate the rescuing. -- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. @@ -267,10 +269,16 @@ function RESCUEHELO:New(carrierunit, helogroupname) self.helogroupname=helogroupname -- Increase ID. - RESCUEHELO.uid=RESCUEHELO.uid+1 + RESCUEHELO.UID=RESCUEHELO.UID+1 + + -- Unique ID of this helo. + self.uid=RESCUEHELO.UID + + -- Save self in static object. Easier to retrieve later. + self.carrier:SetState(self.carrier, string.format("RESCUEHELO_%d", self.uid) , self) -- Set unique spawn alias. - self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.helogroupname, RESCUEHELO.uid) + self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.helogroupname, RESCUEHELO.UID) -- Log ID. self.lid=string.format("RESCUEHELO %s |", self.alias) @@ -295,6 +303,7 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- Debug trace. if false then + self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) @@ -687,7 +696,7 @@ function RESCUEHELO:OnEventLand(EventData) if self:IsRescuing() then - self:T(string.format("Rescue helo %s returned from rescue operation.", groupname)) + self:T(self.lid..string.format("Rescue helo %s returned from rescue operation.", groupname)) end @@ -696,20 +705,18 @@ function RESCUEHELO:OnEventLand(EventData) if self:IsRescuing() then - self:T(string.format("Rescue helo %s returned from rescue operation.", groupname)) + self:T(self.lid..string.format("Rescue helo %s returned from rescue operation.", groupname)) -- Respawn helo at current airbase. - --self.helo=group:RespawnAtCurrentAirbase() - SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 5) + SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 3) else - self:T2(string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true unless a rescue operation finished.", groupname)) + self:T2(self.lid..string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true unless a rescue operation finished.", groupname)) -- Respawn helo at current airbase anyway. if self.respawn then - --self.helo=group:RespawnAtCurrentAirbase() - SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 5) + SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 3) end end @@ -718,8 +725,7 @@ function RESCUEHELO:OnEventLand(EventData) -- Respawn helo at current airbase. if self.respawn then - --self.helo=group:RespawnAtCurrentAirbase() - SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 5) + SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 3) end end @@ -934,7 +940,7 @@ function RESCUEHELO:onafterStatus(From, Event, To) if self.respawn then -- Respawn helo in air. - self.helo:Respawn(nil, true) + self.helo=self.helo:Respawn(nil, true) end @@ -952,7 +958,7 @@ function RESCUEHELO:onafterStatus(From, Event, To) if self.rtb then -- Send helo back to base. - self:RTB() + --self:RTB() -- Switch to false. self.rtb=false @@ -1017,14 +1023,27 @@ function RESCUEHELO:onafterRun(From, Event, To) end ---- Function called when a group is passing a waypoint. ---@param Wrapper.Group#GROUP group Group that passed the waypoint ---@param #RESCUEHELO rescuehelo Rescue helo object. -function RESCUEHELO._TaskRTB(group, rescuehelo) - env.info(string.format("FF Executing TaskRTB for group %s.", group:GetName())) +--- Task to send the helo RTB. +-- @param #RESCUEHELO self +-- @return DCS#Task DCS Task table. +function RESCUEHELO:_TaskRTB() -- Set RTB switch so on next status update, the helo is respawned with RTB waypoints. - rescuehelo.rtb=true + --rescuehelo.rtb=true + + -- Name of the warehouse (static) object. + local carriername=self.carrier:GetName() + + -- Task script. + local DCSScript = {} + DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername) -- The carrier unit that holds the self object. + DCSScript[#DCSScript+1] = string.format('local myhelo = mycarrier:GetState(mycarrier, \"RESCUEHELO_%d\") ', self.uid) -- Get the RECOVERYTANKER self object. + DCSScript[#DCSScript+1] = string.format('myhelo:RTB()') -- Call the function, e.g. myhelo.(self) + + -- Create task. + local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + + return DCSTask -- This made DCS crash to desktop! --rescuehelo:RTB() @@ -1035,7 +1054,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Core.Point#COORDINATE RescueCoord Coordinate where the rescue should happen +-- @param Core.Point#COORDINATE RescueCoord Coordinate where the rescue should happen. function RESCUEHELO:onafterRescue(From, Event, To, RescueCoord) -- Debug message. @@ -1056,7 +1075,8 @@ function RESCUEHELO:onafterRescue(From, Event, To, RescueCoord) RescueTask.params.stopCondition={duration=self.rescueduration} -- Passing waypoint taskfunction - local TaskRTB=self.helo:TaskFunction("RESCUEHELO._TaskRTB", self) + --local TaskRTB=self.helo:TaskFunction("RESCUEHELO._TaskRTB", self) + local TaskRTB=self:_TaskRTB() -- Rescue speed 90% of max possible. local speed=self.helo:GetSpeedMax()*0.9 @@ -1132,7 +1152,7 @@ function RESCUEHELO:onafterRTB(From, Event, To, airbase) end -- Route helo back home. It is respawned! But this is the only way to ensure that it actually lands at the airbase. - self.helo:RouteRTB(airbase) + self:RouteRTB(airbase) end --- On after Stop event. Unhandle events and stop status updates. @@ -1147,6 +1167,42 @@ function RESCUEHELO:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Ejection) end + +--- Route helo back to its home base. +-- @param #RESCUEHELO self +-- @param Wrapper.Airbase#AIRBASE RTBAirbase +-- @param #number Speed Speed. +function RESCUEHELO:RouteRTB(RTBAirbase, Speed) + + -- If speed is not given take 80% of max speed. + local Speed=Speed or self.helo:GetSpeedMax()*0.8 + + -- Curent (from) waypoint. + local coord=self.helo:GetCoordinate() + local PointFrom=coord:WaypointAirTurningPoint(nil, Speed) + + -- Airbase coordinate. + local PointAirbase=RTBAirbase:GetCoordinate():SetAltitude(100):WaypointAirTurningPoint(nil ,Speed) + + -- Landing waypoint. More general than prev version since it should also work with FAPRS and ships. + local PointLanding=RTBAirbase:GetCoordinate():SetAltitude(20):WaypointAirLanding(Speed, RTBAirbase) + + -- Waypoint table. + local Points={PointFrom, PointLanding} + + -- Get group template. + local Template=self.helo:GetTemplate() + + -- Set route points. + Template.route.points=Points + + -- Respawn the group. + self.helo=self.helo:Respawn(Template, true) + + return self +end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 2f5803e85..717d4afcf 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -268,9 +268,9 @@ AIRBASE.PersianGulf = { ["Kerman_Airport"] = "Kerman Airport", ["Shiraz_International_Airport"] = "Shiraz International Airport", ["Sas_Al_Nakheel_Airport"] = "Sas Al Nakheel Airport", - ["Bandar-e-Jask_airfield"] = "Bandar-e-Jask airfield", + ["Bandar_e_Jask_airfield"] = "Bandar-e-Jask airfield", ["Abu_Dhabi_International_Airport"] = "Abu Dhabi International Airport", - ["Al-Bateen_Airport"] = "Al-Bateen Airport", + ["Al_Bateen_Airport"] = "Al-Bateen Airport", ["Kish_International_Airport"] = "Kish International Airport", ["Al_Ain_International_Airport"] = "Al Ain International Airport", ["Lavan_Island_Airport"] = "Lavan Island Airport", @@ -648,31 +648,20 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, verysafe=false end - -- Get the size of an object. - local function _GetObjectSize(unit,mooseobject) - if mooseobject then - unit=unit:GetDCSObject() - end - if unit and unit:isExist() then - local DCSdesc=unit:getDesc() - if DCSdesc.box then - local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x) - local y=DCSdesc.box.max.y+math.abs(DCSdesc.box.min.y) --height - local z=DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z) - return math.max(x,z), x , y, z - end - end - return 0,0,0,0 - end - -- Function calculating the overlap of two (square) objects. - local function _overlap(object1, mooseobject1, object2, mooseobject2, dist) - local l1=_GetObjectSize(object1, mooseobject1) - local l2=_GetObjectSize(object2, mooseobject2) - local safedist=(l1/2+l2/2)*1.1 - local safe = (dist > safedist) - self:T3(string.format("l1=%.1f l2=%.1f s=%.1f d=%.1f ==> safe=%s", l1,l2,safedist,dist,tostring(safe))) - return safe + local function _overlap(object1, object2, dist) + local pos1=object1 --Wrapper.Positionable#POSITIONABLE + local pos2=object2 --Wrapper.Positionable#POSITIONABLE + local r1=pos1:GetBoundingRadius() + local r2=pos2:GetBoundingRadius() + if r1 and r2 then + local safedist=(r1+r2)*1.1 + local safe = (dist > safedist) + self:E(string.format("r1=%.1f r2=%.1f s=%.1f d=%.1f ==> safe=%s", r1, r2, safedist, dist, tostring(safe))) + return safe + else + return true + end end -- Get airport name. @@ -687,7 +676,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Get the aircraft size, i.e. it's longest side of x,z. local aircraft=group:GetUnit(1) - local _aircraftsize, ax,ay,az=_GetObjectSize(aircraft, true) + local _aircraftsize, ax,ay,az=aircraft:GetObjectSize() -- Number of spots we are looking for. Note that, e.g. grouping can require a number different from the group size! local _nspots=nspots or group:GetSize() @@ -733,16 +722,13 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Check all units. for _,unit in pairs(_units) do - -- Unis are now returned as MOOSE units not DCS units! - --local _vec3=unit:getPoint() - --local _coord=COORDINATE:NewFromVec3(_vec3) local _coord=unit:GetCoordinate() local _dist=_coord:Get2DDistance(_spot) - local _safe=_overlap(aircraft, true, unit, true,_dist) + local _safe=_overlap(aircraft, unit, _dist) if markobstacles then - local l,x,y,z=_GetObjectSize(unit) - _coord:MarkToAll(string.format("Unit %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", unit:getName(),x,y,z,l,_dist, _termid, tostring(_safe))) + local l,x,y,z=unit:GetObjectSize() + _coord:MarkToAll(string.format("Unit %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", unit:GetName(),x,y,z,l,_dist, _termid, tostring(_safe))) end if scanunits and not _safe then @@ -752,13 +738,14 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Check all statics. for _,static in pairs(_statics) do + local _static=STATIC:Find(static) local _vec3=static:getPoint() local _coord=COORDINATE:NewFromVec3(_vec3) local _dist=_coord:Get2DDistance(_spot) - local _safe=_overlap(aircraft, true, static, false,_dist) + local _safe=_overlap(aircraft,_static,_dist) if markobstacles then - local l,x,y,z=_GetObjectSize(static) + local l,x,y,z=_static:GetObjectSize() _coord:MarkToAll(string.format("Static %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", static:getName(),x,y,z,l,_dist, _termid, tostring(_safe))) end @@ -769,13 +756,14 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Check all scenery. for _,scenery in pairs(_sceneries) do + local _scenery=SCENERY:Register(scenery:getTypeName(), scenery) local _vec3=scenery:getPoint() local _coord=COORDINATE:NewFromVec3(_vec3) local _dist=_coord:Get2DDistance(_spot) - local _safe=_overlap(aircraft, true, scenery, false,_dist) + local _safe=_overlap(aircraft,_scenery,_dist) if markobstacles then - local l,x,y,z=_GetObjectSize(scenery) + local l,x,y,z=scenery:GetObjectSize(scenery) _coord:MarkToAll(string.format("Scenery %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", scenery:getTypeName(),x,y,z,l,_dist, _termid, tostring(_safe))) end @@ -787,7 +775,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Now check the already given spots so that we do not put a large aircraft next to one we already assigned a nearby spot. for _,_takenspot in pairs(validspots) do local _dist=_takenspot.Coordinate:Get2DDistance(_spot) - local _safe=_overlap(aircraft, true, aircraft, true,_dist) + local _safe=_overlap(aircraft, aircraft, _dist) if not _safe then occupied=true end @@ -797,7 +785,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, if occupied then self:T(string.format("%s: Parking spot id %d occupied.", airport, _termid)) else - self:E(string.format("%s: Parking spot id %d free.", airport, _termid)) + self:I(string.format("%s: Parking spot id %d free.", airport, _termid)) if nvalid<_nspots then table.insert(validspots, {Coordinate=_spot, TerminalID=_termid}) end diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index f812716dc..72da87020 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1937,29 +1937,6 @@ do -- Route methods -- If speed is not given take 80% of max speed. local Speed=Speed or self:GetSpeedMax()*0.8 - - --[[ - local GroupPoint = self:GetVec2() - local GroupVelocity = self:GetUnit(1):GetDesc().speedMax - local PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = GroupVelocity - - local PointTo = {} - local AirbasePointVec2 = RTBAirbase:GetPointVec2() - local AirbaseAirPoint = AirbasePointVec2:WaypointAir( - POINT_VEC3.RoutePointAltType.BARO, - "Land", - "Landing", - Speed or self:GetUnit(1):GetDesc().speedMax - ) - - AirbaseAirPoint["airdromeId"] = RTBAirbase:GetID() - AirbaseAirPoint["speed_locked"] = true - ]] -- Curent (from) waypoint. local coord=self:GetCoordinate() diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 13d803eaf..70d7a3c15 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -428,6 +428,27 @@ function POSITIONABLE:GetBoundingBox() --R2.1 end +--- Get the object size. +-- @param #POSITIONABLE self +-- @return DCS#Distance Max size of object in x, z or 0 if bounding box could not be obtained. +-- @return DCS#Distance Length x or 0 if bounding box could not be obtained. +-- @return DCS#Distance Height y or 0 if bounding box could not be obtained. +-- @return DCS#Distance Width z or 0 if bounding box could not be obtained. +function POSITIONABLE:GetObjectSize() + + -- Get bounding box. + local box=self:GetBoundingBox() + + if box then + local x=box.max.x+math.abs(box.min.x) --length + local y=box.max.y+math.abs(box.min.y) --height + local z=box.max.z+math.abs(box.min.z) --width + return math.max(x,z), x , y, z + end + + return 0,0,0,0 +end + --- Get the bounding radius of the underlying POSITIONABLE DCS Object. -- @param #POSITIONABLE self -- @param #number mindist (Optional) If bounding box is smaller than this value, mindist is returned. From bcfa0f9ac5d605caa5daacd225f9acaf3d856c30 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 29 Jan 2019 17:07:48 +0100 Subject: [PATCH 143/147] RECOVERYTANKER v1.0.5 Recoverytanker: - Added AWACS roll - Added function to set callsign UTILS - Added callsigns enumerator CONTROLLABLE - Added command SetCallsign --- Moose Development/Moose/DCS.lua | 5 +- .../Moose/Ops/RecoveryTanker.lua | 90 +++++++++++++++++-- Moose Development/Moose/Utilities/Utils.lua | 62 +++++++++++++ .../Moose/Wrapper/Controllable.lua | 21 +++++ 4 files changed, 169 insertions(+), 9 deletions(-) diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index 002ea566d..0d508793c 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -1318,7 +1318,4 @@ do -- AI AI = {} --#AI -end -- AI - - - +end -- AI \ No newline at end of file diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index e396691dd..42f690bc8 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -56,6 +56,9 @@ -- @field Core.Point#COORDINATE position Position of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. -- @field #string alias Alias of the spawn group. -- @field #number uid Unique ID of this tanker. +-- @field #boolean awacs If true, the groups gets the enroute task AWACS instead of tanker. +-- @field #number callsignname Number for the callsign name. +-- @field #number callsignnumber Number of the callsign name. -- @extends Core.Fsm#FSM --- Recovery Tanker. @@ -184,6 +187,37 @@ -- -- The maximum update frequency is set to 10 minutes. You can adjust this by @{#RECOVERYTANKER.SetPatternUpdateInterval}. -- Also the pattern will not be updated whilst the carrier is turning or the tanker is currently refueling another unit. +-- +-- ## Callsign +-- +-- The callsign of the tanker can be set via the @{#RECOVERYTANKER.SetCallsign}(*callsignname*, *callsignnumber*) function. Both parameters are *numbers*. +-- The first parameter *callsignname* defines the name (1=Texaco, 2=Arco, 3=Shell). The second (optional) parameter specifies the first number and has to be between 1-9. +-- Also see [DCS_enum_callsigns](https://wiki.hoggitworld.com/view/DCS_enum_callsigns) and [DCS_command_setCallsign](https://wiki.hoggitworld.com/view/DCS_command_setCallsign). +-- +-- TexacoStennis:SetCAllsign(CALLSIGN.Tanker.Arco) +-- +-- For convenience, MOOSE has a CALLSIGN enumerator introduced. +-- +-- ## AWACS +-- +-- You can use the class also to have an AWACS orbiting overhead the carrier. This requires to add the @{#RECOVERYTANKER.SetAWACS}() function to the script, which sets the enroute tasks AWACS +-- as soon as the aircraft enters its pattern. +-- +-- A simple script could look like this: +-- +-- -- E-2D at USS Stennis spawning in air. +-- local awacsStennis=RECOVERYTANKER:New("USS Stennis", "E2D Group") +-- +-- -- Custom settings: +-- awacsStennis:SetAWACS() +-- awacsStennis:SetCallsign(CALLSIGN.AWACS.Wizard, 1) +-- awacsStennis:SetTakeoffAir() +-- awacsStennis:SetAltitude(20000) +-- awacsStennis:SetRadio(262) +-- awacsStennis:SetTACAN(2, "WIZ") +-- +-- -- Start AWACS. +-- awacsStennis:Start() -- -- # Finite State Machine -- @@ -255,6 +289,9 @@ RECOVERYTANKER = { position = nil, alias = nil, uid = 0, + awacs = nil, + callsignname = nil, + callsignnumber = nil, } --- Unique ID (global). @@ -263,7 +300,7 @@ RECOVERYTANKER.UID=0 --- Class version. -- @field #string version -RECOVERYTANKER.version="1.0.4" +RECOVERYTANKER.version="1.0.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -336,6 +373,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetPatternUpdateDistance() self:SetPatternUpdateHeading() self:SetPatternUpdateInterval() + self:SetAWACS(false) -- Debug trace. if false then @@ -555,6 +593,30 @@ function RECOVERYTANKER:SetHomeBase(airbase) return self end +--- Set that the group takes the roll of an AWACS instead of a refueling tanker. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true or nil, set roll AWACS. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetAWACS(switch) + if switch==nil or switch==true then + self.awacs=true + else + self.awacs=false + end + return self +end + +--- Set callsign of the tanker group. +-- @param #RECOVERYTANKER self +-- @param #number callsignname Number +-- @param #number callsignnumber Number +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetCallsign(callsignname, callsignnumber) + self.callsignname=callsignname + self.callsignnumber=callsignnumber + return self +end + --- Set takeoff type. -- @param #RECOVERYTANKER self -- @param #number takeofftype Takeoff type. @@ -813,6 +875,11 @@ function RECOVERYTANKER:onafterStart(From, Event, To) self:_ActivateTACAN(2) end + -- Set callsign. + if self.callsignname then + self.tanker:CommandSetCallsign(self.callsignname, self.callsignnumber, 2) + end + -- Get initial orientation and position of carrier. self.orientation=self.carrier:GetOrientationX() self.orientlast=self.carrier:GetOrientationX() @@ -877,6 +944,11 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) self:_ActivateTACAN(3) end + -- Set callsign. + if self.callsignname then + self.tanker:CommandSetCallsign(self.callsignname, self.callsignnumber, 3) + end + -- Update Pattern in 2 seconds. Need to give a bit time so that the respawned group is in the game. self:__PatternUpdate(2) end @@ -976,10 +1048,13 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) self.tanker:WayPointInitialize(wp) -- Task combo. - local tasktanker = self.tanker:EnRouteTaskTanker() + local taskroll = self.tanker:EnRouteTaskTanker() + if self.awacs then + taskroll=self.tanker:EnRouteTaskAWACS() + end local taskroute = self.tanker:TaskRoute(wp) - -- Note that tasktanker has to come first. Otherwise it does not work! - local taskcombo = self.tanker:TaskCombo({tasktanker, taskroute}) + -- Note that the order is important here! tasktanker has to come first. Otherwise it does not work. + local taskcombo = self.tanker:TaskCombo({taskroll, taskroute}) -- Set task. self.tanker:SetTask(taskcombo, 1) @@ -1071,8 +1146,13 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) -- Create tanker beacon and activate TACAN. if self.TACANon then - self:_ActivateTACAN(2) + self:_ActivateTACAN(3) end + + -- Set callsign. + if self.callsignname then + self.tanker:CommandSetCallsign(self.callsignname, self.callsignnumber, 3) + end -- Initial route. SCHEDULER:New(nil, self._InitRoute, {self, -self.distStern+UTILS.NMToMeters(3)}, 2) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index ca20c1b7f..746d1c7c4 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -56,6 +56,68 @@ DCSMAP = { PersianGulf="PersianGulf" } + +--- See [DCS_enum_callsigns](https://wiki.hoggitworld.com/view/DCS_enum_callsigns) +-- @type CALLSIGN +-- @field #table Aircraft Aircraft callsigns. +-- @field #table AWACS AWACS callsigns. +-- @field #table Tanker Tanker callsigns. +-- @field #table JTAC JTAC callsigns. +CALLSIGN={ + -- Aircraft + Aircraft={ + Enfield=1, + Springfield=2, + Uzi=3, + Cold=4, + Dodge=5, + Ford=6, + Chevy=7, + Pontiac=8, + -- A-10A or A-10C + Hawg=9, + Boar=10, + Pig=11, + Tusk=12, + }, + -- AWACS + AWACS={ + Overloard=1, + Magic=2, + Wizard=3, + Focus=4, + Darkstar=5, + }, + -- Tanker + Tanker={ + Texaco=1, + Arco=2, + Shell=3, + }, + -- JTAC + JTAC={ + Axeman=1, + Darknight=2, + Warrier=3, + Pointer=4, + Eyeball=5, + Moonbeam=6, + Whiplash=7, + Finger=8, + Pinpoint=9, + Ferret=10, + Shaba=11, + Playboy=12, + Hammer=13, + Jaguar=14, + Deathstar=15, + Anvil=16, + Firefly=17, + Mantis=18, + Badger=19, + }, +} --#CALLSIGN + --- Utilities static class. -- @type UTILS UTILS = { diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index eac3ce207..ee56856b2 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -761,6 +761,27 @@ function CONTROLLABLE:CommandDeactivateICLS(Delay) return self end +--- Set callsign of the CONTROLLABLE. See [DCS_command_setCallsign](https://wiki.hoggitworld.com/view/DCS_command_setCallsign) +-- @param #CONTROLLABLE self +-- @param DCS#CALLSIGN CallName Number corresponding the the callsign identifier you wish this group to be called. +-- @param #number CallNumber The number value the group will be referred to as. Only valid numbers are 1-9. For example Uzi **5**-1. Default 1. +-- @param #number Delay (Optional) Delay in seconds before the callsign is set. Default is immediately. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandSetCallsign(CallName, CallNumber, Delay) + self:F() + + -- Command to set the callsign. + local CommandSetCallsign={id='SetCallsign', params={callname=CallName, callnumber=CallNumber or 1}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandSetCallsign, {self, CallName, CallNumber}, Delay) + else + self:SetCommand(CommandSetCallsign) + end + + return self +end + -- TASKS FOR AIR CONTROLLABLES --- (AIR) Attack a Controllable. From f2b2b1974d183bcf6ece350c4b458a9c51cddb17 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 29 Jan 2019 19:58:06 +0100 Subject: [PATCH 144/147] Improvements in the attack methods! This is going to be great! --- Moose Development/Moose/AI/AI_A2G_BAI.lua | 22 +++++++++++------ Moose Development/Moose/AI/AI_A2G_CAS.lua | 22 +++++++++++------ .../Moose/AI/AI_A2G_Dispatcher.lua | 3 ++- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 24 ++++++++++++------- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index 88f9bf491..efd48c397 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -120,14 +120,22 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit EngageRoute[#EngageRoute+1] = ToWP local AttackTasks = {} + + self.AttackSetUnit.AttackIndex = self.AttackSetUnit.AttackIndex and self.AttackSetUnit.AttackIndex + 1 or 1 - for OrderedID, AttackUnit in ipairs( self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) ) do - if AttackUnit then - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:T( { "BAI Unit:", AttackUnit:GetName() } ) - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) - break - end + local AttackSetUnitPerThreatLevel = self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + + local AttackUnit = AttackSetUnitPerThreatLevel[self.AttackSetUnit.AttackIndex] + + if not AttackUnit then + self.AttackSetUnit.AttackIndex = 1 + AttackUnit = AttackSetUnitPerThreatLevel[self.AttackSetUnit.AttackIndex] + end + + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "BAI Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) end end diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 58069c5b6..8d483f317 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -116,14 +116,22 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit EngageRoute[#EngageRoute+1] = ToWP local AttackTasks = {} + + self.AttackSetUnit.AttackIndex = self.AttackSetUnit.AttackIndex and self.AttackSetUnit.AttackIndex + 1 or 1 - for OrderedID, AttackUnit in ipairs( self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) ) do - if AttackUnit then - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:F( { "CAS Unit:", AttackUnit:GetName() } ) - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) - break - end + local AttackSetUnitPerThreatLevel = self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + + local AttackUnit = AttackSetUnitPerThreatLevel[self.AttackSetUnit.AttackIndex] + + if not AttackUnit then + self.AttackSetUnit.AttackIndex = 1 + AttackUnit = AttackSetUnitPerThreatLevel[self.AttackSetUnit.AttackIndex] + end + + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:F( { "CAS Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) end end diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 37f96023f..94beddcbc 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3557,13 +3557,14 @@ do -- AI_A2G_DISPATCHER function Fsm:onafterRTB( Defender, From, Event, To ) self:F({"Defender RTB", Defender:GetName()}) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) local DefenderName = Defender:GetName() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) + self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + Dispatcher:ClearDefenderTaskTarget( Defender ) end diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index c459918be..124fe5049 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -173,15 +173,23 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni EngageRoute[#EngageRoute+1] = ToWP local AttackTasks = {} + + self.AttackSetUnit.AttackIndex = self.AttackSetUnit.AttackIndex and self.AttackSetUnit.AttackIndex + 1 or 1 + if self.AttackSetUnit.AttackIndex > self.AttackSetUnit:Count() then + self.AttackSetUnit.AttackIndex = 1 + end + + local AttackSetUnitPerThreatLevel = self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) - for OrderedID, AttackUnit in ipairs( self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) ) do - if AttackUnit then - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - local HasRadar = AttackUnit:HasSEAD() - if HasRadar then - self:F( { "SEAD Unit:", AttackUnit:GetName() } ) - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) - break + for AttackUnitID, AttackUnit in ipairs( AttackSetUnitPerThreatLevel ) do + if AttackUnitID >= self.AttackSetUnit.AttackIndex then + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + local HasRadar = AttackUnit:HasSEAD() + if HasRadar then + self:F( { "SEAD Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) + end end end end From e325b2192b8f7bc53ad0241ba9a95ce6dc628491 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 30 Jan 2019 20:10:41 +0100 Subject: [PATCH 145/147] AIRBOSS, RECOVERYTANKER, RESCUEHELO, RANGE AIRBOSS v0.9.3 RECOVERYTANKER v1.0.6 RESCUEHELO v1.0.4 RANGE v1.2.4 SPAWN added modex GROUP added modex respawn --- Moose Development/Moose/AI/AI_Air.lua | 2 +- Moose Development/Moose/Core/Spawn.lua | 24 +- Moose Development/Moose/Functional/Range.lua | 382 ++++++++++++------ Moose Development/Moose/Ops/Airboss.lua | 190 +++++++-- .../Moose/Ops/RecoveryTanker.lua | 18 +- Moose Development/Moose/Ops/RescueHelo.lua | 40 +- Moose Development/Moose/Utilities/Utils.lua | 17 +- Moose Development/Moose/Wrapper/Airbase.lua | 7 +- Moose Development/Moose/Wrapper/Group.lua | 31 +- 9 files changed, 533 insertions(+), 178 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index bbfadc5a0..2c708d2fc 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -7,7 +7,7 @@ -- === -- -- @module AI.AI_Air --- @image AI_Air_Operations.JPG +-- @image MOOSE.JPG --- @type AI_AIR -- @extends Core.Fsm#FSM_CONTROLLABLE diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index d69ae0097..d65aae7a2 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -325,6 +325,7 @@ function SPAWN:New( SpawnTemplatePrefix ) self.SpawnInitFreq = nil -- No special frequency. self.SpawnInitModu = nil -- No special modulation. self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitModex = nil self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -376,6 +377,7 @@ function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) self.SpawnInitFreq = nil -- No special frequency. self.SpawnInitModu = nil -- No special modulation. self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitModex = nil self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -430,6 +432,7 @@ function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPr self.SpawnInitFreq = nil -- No special frequency. self.SpawnInitModu = nil -- No special modulation. self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitModex = nil self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -642,6 +645,19 @@ function SPAWN:InitRadioModulation(modulation) return self end +--- Sets the modex of the first unit of the group. If more units are in the group, the number is increased by one with every unit. +-- @param #SPAWN self +-- @param #number modex Modex of the first unit. +-- @return #SPAWN self +function SPAWN:InitModex(modex) + + if modex then + self.SpawnInitModex=tonumber(modex) + end + + return self +end + --- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups. -- @param #SPAWN self @@ -1218,7 +1234,13 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) SpawnTemplate.units[UnitID].skill = self.SpawnInitSkill end end - + + -- Set tail number. + if self.SpawnInitModex then + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].onboard_num = string.format("%03d", self.SpawnInitModex+(UnitID-1)) + end + end -- Set radio comms on/off. if self.SpawnInitRadio then diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 394f134f2..a3e73b21a 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -11,7 +11,7 @@ -- -- ## Features: -- --- * Impact points of bombs, rockets and missils are recorded and distance to closest range target is measured and reported to the player. +-- * Impact points of bombs, rockets and missiles are recorded and distance to closest range target is measured and reported to the player. -- * Number of hits on strafing passes are counted and reported. Also the percentage of hits w.r.t fired shots is evaluated. -- * Results of all bombing and strafing runs are stored and top 10 results can be displayed. -- * Range targets can be marked by smoke. @@ -56,9 +56,9 @@ -- @field #table strafeStatus Table containing the current strafing target a player as assigned to. -- @field #table strafePlayerResults Table containing the strafing results of each player. -- @field #table bombPlayerResults Table containing the bombing results of each player. --- @field #table PlayerSettings Indiviual player settings. +-- @field #table PlayerSettings Individual player settings. -- @field #number dtBombtrack Time step [sec] used for tracking released bomb/rocket positions. Default 0.005 seconds. --- @field #number BombtrackThreshold Bombs/rockets/missiles are only tracked if player-range distance is smaller than this threashold [m]. Default 25000 m. +-- @field #number BombtrackThreshold Bombs/rockets/missiles are only tracked if player-range distance is smaller than this threshold [m]. Default 25000 m. -- @field #number Tmsg Time [sec] messages to players are displayed. Default 30 sec. -- @field #string examinergroupname Name of the examiner group which should get all messages. -- @field #boolean examinerexclusive If true, only the examiner gets messages. If false, clients and examiner get messages. @@ -75,10 +75,11 @@ -- @field #boolean trackbombs If true (default), all bomb types are tracked and impact point to closest bombing target is evaluated. -- @field #boolean trackrockets If true (default), all rocket types are tracked and impact point to closest bombing target is evaluated. -- @field #boolean trackmissiles If true (default), all missile types are tracked and impact point to closest bombing target is evaluated. +-- @field #boolean defaultsmokebomb If true, initialize player settings to smoke bomb. -- @extends Core.Base#BASE --- Enables a mission designer to easily set up practice ranges in DCS. A new RANGE object can be created with the @{#RANGE.New}(rangename) contructor. --- The parameter "rangename" defindes the name of the range. It has to be unique since this is also the name displayed in the radio menu. +-- The parameter "rangename" defines the name of the range. It has to be unique since this is also the name displayed in the radio menu. -- -- Generally, a range consists of strafe pits and bombing targets. For strafe pits the number of hits for each pass is counted and tabulated. -- For bombing targets, the distance from the impact point of the bomb, rocket or missile to the closest range target is measured and tabulated. @@ -89,12 +90,12 @@ -- **IMPORTANT** -- -- Due to a DCS bug, it is not possible to directly monitor when a player enters a plane. So in a mission with client slots, it is vital that --- a player first enters as spector and **after that** jumps into the slot of his aircraft! +-- a player first enters as spectator or hits ESC twice and **after that** jumps into the slot of his aircraft! -- If that is not done, the script is not started correctly. This can be checked by looking at the radio menues. If the mission was entered correctly, -- there should be an "On the Range" menu items in the "F10. Other..." menu. -- -- ## Strafe Pits --- Each strafe pit can consist of multiple targets. Often one findes two or three strafe targets next to each other. +-- Each strafe pit can consist of multiple targets. Often one finds two or three strafe targets next to each other. -- -- A strafe pit can be added to the range by the @{#RANGE.AddStrafePit}(*targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*) function. -- @@ -104,7 +105,7 @@ -- If the parameter *heading* is passed as **nil**, the heading is automatically taken from the heading of the first target unit as defined in the ME. -- The parameter *inverseheading* turns the heading around by 180 degrees. This is sometimes useful, since the default heading of strafe target units point in the -- wrong/opposite direction. --- * The parameter *goodpass* defines the number of hits a pilot has to achive during a run to be judged as a "good" pass. +-- * The parameter *goodpass* defines the number of hits a pilot has to achieve during a run to be judged as a "good" pass. -- * The last parameter *foulline* sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted! -- -- Another function to add a strafe pit is @{#RANGE.AddStrafePitGroup}(*group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*). Here, @@ -151,7 +152,7 @@ -- * "F2. My Settings": Player specific settings. -- * "F3. Stats" Player: statistics and scores. -- * "Range Information": Information about the range, such as bearing and range. Also range and player specific settings are displayed. --- * "Weather Report": Temperatur, wind and QFE pressure information is provided. +-- * "Weather Report": Temperature, wind and QFE pressure information is provided. -- -- ## Examples -- @@ -243,6 +244,7 @@ RANGE={ trackbombs=true, trackrockets=true, trackmissiles=true, + defaultsmokebomb=true, } --- Default range parameters. @@ -266,19 +268,25 @@ RANGE.Defaults={ -- @field #table Names RANGE.Names={} ---- Main radio menu. --- @field #table MenuF10 +--- Main radio menu on group level. +-- @field #table MenuF10 Root menu table on group level. RANGE.MenuF10={} +--- Main radio menu on mission level. +-- @field #table MenuF10Root Root menu on mission level. +RANGE.MenuF10Root=nil + --- Some ID to identify who we are in output of the DCS.log file. -- @field #string id RANGE.id="RANGE | " --- Range script version. -- @field #string version -RANGE.version="1.2.3" +RANGE.version="1.2.4" --TODO list: +--TODO: Verbosity level for messages. +--TODO: Add option for default settings such as smoke off. --TODO: Add custom weapons, which can be specified by the user. --TODO: Check if units are still alive. --DONE: Add statics for strafe pits. @@ -310,6 +318,9 @@ function RANGE:New(rangename) local text=string.format("RANGE script version %s - creating new RANGE object of name: %s.", RANGE.version, self.rangename) self:E(RANGE.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) + + -- Defaults + self:SetDefaultPlayerSmokeBomb() -- Return object. return self @@ -317,93 +328,102 @@ end --- Initializes number of targets and location of the range. Starts the event handlers. -- @param #RANGE self -function RANGE:Start() +-- @param #number delay Delay in seconds, before the RANGE is started. Default immediately. +-- @return self +function RANGE:Start(delay) self:F() - -- Location/coordinate of range. - local _location=nil + if delay and delay>0 then + SCHEDULER:New(nil, self.Start, {self}, delay) + else - -- Count bomb targets. - local _count=0 - for _,_target in pairs(self.bombingTargets) do - _count=_count+1 + -- Location/coordinate of range. + local _location=nil - -- Get range location. - if _location==nil then - _location=_target.target:GetCoordinate() --Core.Point#COORDINATE - end - end - self.nbombtargets=_count - - -- Count strafing targets. - _count=0 - for _,_target in pairs(self.strafeTargets) do - _count=_count+1 - - for _,_unit in pairs(_target.targets) do + -- Count bomb targets. + local _count=0 + for _,_target in pairs(self.bombingTargets) do + _count=_count+1 + + -- Get range location. if _location==nil then - _location=_unit:GetCoordinate() + _location=_target.target:GetCoordinate() --Core.Point#COORDINATE end end - end - self.nstrafetargets=_count - - -- Location of the range. We simply take the first unit/target we find if it was not explicitly specified by the user. - if self.location==nil then - self.location=_location - end - - if self.location==nil then - local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) - self:E(RANGE.id..text) - return - end - - -- Define a MOOSE zone of the range. - if self.rangezone==nil then - self.rangezone=ZONE_RADIUS:New(self.rangename, {x=self.location.x, y=self.location.z}, self.rangeradius) - end - - -- Starting range. - local text=string.format("Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) - self:E(RANGE.id..text) - MESSAGE:New(text,10):ToAllIf(self.Debug) - - -- Event handling. - if self.eventmoose then - -- Events are handled my MOOSE. - self:T(RANGE.id.."Events are handled by MOOSE.") - self:HandleEvent(EVENTS.Birth) - self:HandleEvent(EVENTS.Hit) - self:HandleEvent(EVENTS.Shot) - else - -- Events are handled directly by DCS. - self:T(RANGE.id.."Events are handled directly by DCS.") - world.addEventHandler(self) - end - - -- Make bomb target move randomly within the range zone. - for _,_target in pairs(self.bombingTargets) do - - -- Check if it is a static object. - local _static=self:_CheckStatic(_target.target:GetName()) + self.nbombtargets=_count - if _target.move and _static==false and _target.speed>1 then - local unit=_target.target --Wrapper.Unit#UNIT - _target.target:PatrolZones({self.rangezone}, _target.speed*0.75, "Off road") + -- Count strafing targets. + _count=0 + for _,_target in pairs(self.strafeTargets) do + _count=_count+1 + + for _,_unit in pairs(_target.targets) do + if _location==nil then + _location=_unit:GetCoordinate() + end + end + end + self.nstrafetargets=_count + + -- Location of the range. We simply take the first unit/target we find if it was not explicitly specified by the user. + if self.location==nil then + self.location=_location + end + + if self.location==nil then + local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) + self:E(RANGE.id..text) + return + end + + -- Define a MOOSE zone of the range. + if self.rangezone==nil then + self.rangezone=ZONE_RADIUS:New(self.rangename, {x=self.location.x, y=self.location.z}, self.rangeradius) + end + + -- Starting range. + local text=string.format("Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) + self:I(RANGE.id..text) + MESSAGE:New(text,10):ToAllIf(self.Debug) + + -- Event handling. + if self.eventmoose then + -- Events are handled my MOOSE. + self:T(RANGE.id.."Events are handled by MOOSE.") + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Hit) + self:HandleEvent(EVENTS.Shot) + else + -- Events are handled directly by DCS. + self:T(RANGE.id.."Events are handled directly by DCS.") + world.addEventHandler(self) + end + + -- Make bomb target move randomly within the range zone. + for _,_target in pairs(self.bombingTargets) do + + -- Check if it is a static object. + local _static=self:_CheckStatic(_target.target:GetName()) + + if _target.move and _static==false and _target.speed>1 then + local unit=_target.target --Wrapper.Unit#UNIT + _target.target:PatrolZones({self.rangezone}, _target.speed*0.75, "Off road") + end + + end + + -- Debug mode: smoke all targets and range zone. + if self.Debug then + self:_MarkTargetsOnMap() + self:_SmokeBombTargets() + self:_SmokeStrafeTargets() + self:_SmokeStrafeTargetBoxes() + self.rangezone:SmokeZone(SMOKECOLOR.White) end end - -- Debug mode: smoke all targets and range zone. - if self.Debug then - self:_MarkTargetsOnMap() - self:_SmokeBombTargets() - self:_SmokeStrafeTargets() - self:_SmokeStrafeTargetBoxes() - self.rangezone:SmokeZone(SMOKECOLOR.White) - end - + return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -412,144 +432,199 @@ end --- Set maximal strafing altitude. Player entering a strafe pit above that altitude are not registered for a valid pass. -- @param #RANGE self -- @param #number maxalt Maximum altitude AGL in meters. Default is 914 m= 3000 ft. +-- @return #RANGE self function RANGE:SetMaxStrafeAlt(maxalt) self.strafemaxalt=maxalt or RANGE.Defaults.strafemaxalt + return self end --- Set time interval for tracking bombs. A smaller time step increases accuracy but needs more CPU time. -- @param #RANGE self -- @param #number dt Time interval in seconds. Default is 0.005 s. +-- @return #RANGE self function RANGE:SetBombtrackTimestep(dt) self.dtBombtrack=dt or RANGE.Defaults.dtBombtrack + return self end --- Set time how long (most) messages are displayed. -- @param #RANGE self -- @param #number time Time in seconds. Default is 30 s. +-- @return #RANGE self function RANGE:SetMessageTimeDuration(time) self.Tmsg=time or RANGE.Defaults.Tmsg + return self end --- Set messages to examiner. The examiner will receive messages from all clients. -- @param #RANGE self -- @param #string examinergroupname Name of the group of the examiner. -- @param #boolean exclusively If true, messages are send exclusively to the examiner, i.e. not to the clients. +-- @return #RANGE self function RANGE:SetMessageToExaminer(examinergroupname, exclusively) self.examinergroupname=examinergroupname self.examinerexclusive=exclusively + return self end --- Set max number of player results that are displayed. -- @param #RANGE self -- @param #number nmax Number of results. Default is 10. +-- @return #RANGE self function RANGE:SetDisplayedMaxPlayerResults(nmax) self.ndisplayresult=nmax or RANGE.Defaults.ndisplayresult + return self end --- Set range radius. Defines the area in which e.g. bomb impacts are smoked. -- @param #RANGE self -- @param #number radius Radius in km. Default 5 km. +-- @return #RANGE self function RANGE:SetRangeRadius(radius) self.rangeradius=radius*1000 or RANGE.Defaults.rangeradius + return self +end + +--- Set player setting whether bomb impact points are smoked or not +-- @param #RANGE self +-- @param #boolean If true nor nil default is to smoke impact points of bombs. +-- @return #RANGE self +function RANGE:SetDefaultPlayerSmokeBomb(switch) + if switch==true or switch==nil then + self.defaultsmokebomb=true + else + self.defaultsmokebomb=false + end + return self end --- Set bomb track threshold distance. Bombs/rockets/missiles are only tracked if player-range distance is less than this distance. Default 25 km. -- @param #RANGE self -- @param #number distance Threshold distance in km. Default 25 km. +-- @return #RANGE self function RANGE:SetBombtrackThreshold(distance) self.BombtrackThreshold=distance*1000 or 25*1000 + return self end --- Set range location. If this is not done, one (random) unit position of the range is used to determine the location of the range. -- The range location determines the position at which the weather data is evaluated. -- @param #RANGE self -- @param Core.Point#COORDINATE coordinate Coordinate of the range. +-- @return #RANGE self function RANGE:SetRangeLocation(coordinate) self.location=coordinate + return self end --- Set range zone. For example, no bomb impact points are smoked if a bomb falls outside of this zone. -- If a zone is not explicitly specified, the range zone is determined by its location and radius. -- @param #RANGE self -- @param Core.Zone#ZONE zone MOOSE zone defining the range perimeters. +-- @return #RANGE self function RANGE:SetRangeZone(zone) self.rangezone=zone + return self end --- Set smoke color for marking bomb targets. By default bomb targets are marked by red smoke. -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Red. +-- @return #RANGE self function RANGE:SetBombTargetSmokeColor(colorid) self.BombSmokeColor=colorid or SMOKECOLOR.Red + return self end --- Set smoke color for marking strafe targets. By default strafe targets are marked by green smoke. -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Green. +-- @return #RANGE self function RANGE:SetStrafeTargetSmokeColor(colorid) self.StrafeSmokeColor=colorid or SMOKECOLOR.Green + return self end --- Set smoke color for marking strafe pit approach boxes. By default strafe pit boxes are marked by white smoke. -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.White. +-- @return #RANGE self function RANGE:SetStrafePitSmokeColor(colorid) self.StrafePitSmokeColor=colorid or SMOKECOLOR.White + return self end --- Set time delay between bomb impact and starting to smoke the impact point. -- @param #RANGE self -- @param #number delay Time delay in seconds. Default is 3 seconds. +-- @return #RANGE self function RANGE:SetSmokeTimeDelay(delay) self.TdelaySmoke=delay or RANGE.Defaults.TdelaySmoke + return self end --- Enable debug modus. -- @param #RANGE self +-- @return #RANGE self function RANGE:DebugON() self.Debug=true + return self end --- Disable debug modus. -- @param #RANGE self +-- @return #RANGE self function RANGE:DebugOFF() self.Debug=false + return self end --- Enables tracking of all bomb types. Note that this is the default setting. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackBombsON() self.trackbombs=true + return self end --- Disables tracking of all bomb types. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackBombsOFF() self.trackbombs=false + return self end --- Enables tracking of all rocket types. Note that this is the default setting. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackRocketsON() self.trackrockets=true + return self end --- Disables tracking of all rocket types. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackRocketsOFF() self.trackrockets=false + return self end --- Enables tracking of all missile types. Note that this is the default setting. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackMissilesON() self.trackmissiles=true + return self end --- Disables tracking of all missile types. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackMissilesOFF() self.trackmissiles=false + return self end @@ -564,6 +639,7 @@ end -- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. -- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. -- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. +-- @return #RANGE self function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) self:F({targetnames=targetnames, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) @@ -681,6 +757,8 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe local text=string.format("Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline) self:T(RANGE.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) + + return self end @@ -696,6 +774,7 @@ end -- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. -- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. -- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. +-- @return #RANGE self function RANGE:AddStrafePitGroup(group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) self:F({group=group, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) @@ -721,6 +800,7 @@ function RANGE:AddStrafePitGroup(group, boxlength, boxwidth, heading, inversehea self:AddStrafePit(_names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) end + return self end --- Add bombing target(s) to range. @@ -728,6 +808,7 @@ end -- @param #table targetnames Table containing names of unit or static objects serving as bomb targets. -- @param #number goodhitrange (Optional) Max distance from target unit (in meters) which is considered as a good hit. Default is 25 m. -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +-- @return #RANGE self function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) self:F({targetnames=targetnames, goodhitrange=goodhitrange, randommove=randommove}) @@ -757,6 +838,8 @@ function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) end end + + return self end --- Add a unit or static object as bombing target. @@ -764,6 +847,7 @@ end -- @param Wrapper.Positionable#POSITIONABLE unit Positionable (unit or static) of the strafe target. -- @param #number goodhitrange Max distance from unit which is considered as a good hit. -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +-- @return #RANGE self function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) self:F({unit=unit, goodhitrange=goodhitrange, randommove=randommove}) @@ -798,6 +882,8 @@ function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) -- Insert target to table. table.insert(self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed}) + + return self end --- Add all units of a group as bombing targets. @@ -805,6 +891,7 @@ end -- @param Wrapper.Group#GROUP group Group of bombing targets. -- @param #number goodhitrange Max distance from unit which is considered as a good hit. -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +-- @return #RANGE self function RANGE:AddBombingTargetGroup(group, goodhitrange, randommove) self:F({group=group, goodhitrange=goodhitrange, randommove=randommove}) @@ -819,6 +906,7 @@ function RANGE:AddBombingTargetGroup(group, goodhitrange, randommove) end end + return self end --- Measures the foule line distance between two unit or static objects. @@ -971,11 +1059,12 @@ function RANGE:OnEventBirth(EventData) -- By default, some bomb impact points and do not flare each hit on target. self.PlayerSettings[_playername]={} - self.PlayerSettings[_playername].smokebombimpact=true + self.PlayerSettings[_playername].smokebombimpact=self.defaultsmokebomb self.PlayerSettings[_playername].flaredirecthits=false self.PlayerSettings[_playername].smokecolor=SMOKECOLOR.Blue self.PlayerSettings[_playername].flarecolor=FLARECOLOR.Red self.PlayerSettings[_playername].delaysmoke=true + self.PlayerSettings[_playername].messages=true -- Start check in zone timer. if self.planes[_uid] ~= true then @@ -1042,7 +1131,7 @@ function RANGE:OnEventHit(EventData) if _currentTarget.pastfoulline==false and _unit and _playername then local _d=_currentTarget.zone.foulline local text=string.format("%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname(_unitName), _d, targetname) - self:_DisplayMessageToGroup(_unit, text, 10) + self:_DisplayMessageToGroup(_unit, text) self:T2(RANGE.id..text) _currentTarget.pastfoulline=true end @@ -1320,7 +1409,7 @@ function RANGE:_DisplayMyStrafePitResults(_unitName) end -- Send message to group. - self:_DisplayMessageToGroup(_unit, _message, nil, true) + self:_DisplayMessageToGroup(_unit, _message, nil, true, true) end end @@ -1376,7 +1465,7 @@ function RANGE:_DisplayStrafePitResults(_unitName) end -- Send message. - self:_DisplayMessageToGroup(_unit, _message, nil, true) + self:_DisplayMessageToGroup(_unit, _message, nil, true, true) end end @@ -1433,7 +1522,7 @@ function RANGE:_DisplayMyBombingResults(_unitName) end -- Send message. - self:_DisplayMessageToGroup(_unit, _message, nil, true) + self:_DisplayMessageToGroup(_unit, _message, nil, true, true) end end @@ -1489,7 +1578,7 @@ function RANGE:_DisplayBombingResults(_unitName) end -- Send message. - self:_DisplayMessageToGroup(_unit, _message, nil, true) + self:_DisplayMessageToGroup(_unit, _message, nil, true, true) end end @@ -1566,7 +1655,7 @@ function RANGE:_DisplayRangeInfo(_unitname) text=text..textdelay -- Send message to player group. - self:_DisplayMessageToGroup(unit, text, nil, true) + self:_DisplayMessageToGroup(unit, text, nil, true, true) -- Debug output. self:T2(RANGE.id..text) @@ -1603,7 +1692,7 @@ function RANGE:_DisplayBombTargets(_unitname) end end - self:_DisplayMessageToGroup(_unit,_text, nil, true) + self:_DisplayMessageToGroup(_unit,_text, nil, true, true) end end @@ -1643,7 +1732,7 @@ function RANGE:_DisplayStrafePits(_unitname) _text=_text..string.format("\n- %s: %s - heading %03d",_strafepit.name, mycoord, heading) end - self:_DisplayMessageToGroup(_unit,_text, nil, true) + self:_DisplayMessageToGroup(_unit,_text, nil, true, true) end end @@ -1705,7 +1794,7 @@ function RANGE:_DisplayRangeWeather(_unitname) end -- Send message to player group. - self:_DisplayMessageToGroup(unit, text, nil, true) + self:_DisplayMessageToGroup(unit, text, nil, true, true) -- Debug output. self:T2(RANGE.id..text) @@ -1749,7 +1838,7 @@ function RANGE:_CheckInZone(_unitName) local unitinzone=_unit:IsInZone(zone) and unitalt <= self.strafemaxalt and towardspit -- Debug output - local text=string.format("Checking stil in zone. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _unitName, _playername, tostring(unitinzone), unitalt, deltaheading) + local text=string.format("Checking still in zone. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _unitName, _playername, tostring(unitinzone), unitalt, deltaheading) self:T2(RANGE.id..text) -- Check if player is in strafe zone and below max alt. @@ -1894,12 +1983,33 @@ function RANGE:_AddF10Commands(_unitName) -- Enable switch so we don't do this twice. self.MenuAddedTo[_gid] = true - - -- Main F10 menu: F10/On the Range// - if RANGE.MenuF10[_gid] == nil then - RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") - end - local _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid]) + + -- Range root menu path. + local _rangePath=nil + + if RANGE.MenuF10Root then + + ------------------- + -- MISSION LEVEL -- + ------------------- + + _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10Root) + + else + + ----------------- + -- GROUP LEVEL -- + ----------------- + + -- Main F10 menu: F10/On the Range// + if RANGE.MenuF10[_gid] == nil then + RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") + end + _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid]) + + end + + local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Statistics", _rangePath) local _markPath = missionCommands.addSubMenuForGroup(_gid, "Mark Targets", _rangePath) local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath) @@ -1932,9 +2042,11 @@ function RANGE:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "White Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.White) missionCommands.addCommandForGroup(_gid, "Yellow Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Yellow) -- F10/On the Range//My Settings/ - missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) + 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) + missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) + missionCommands.addCommandForGroup(_gid, "All Messages On/Off", _settingsPath, self._MessagesToPlayerOnOff, 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._DisplayRangeWeather, self, _unitName) @@ -2114,7 +2226,7 @@ function RANGE:_ResetRangeStats(_unitName) self.strafePlayerResults[_playername] = nil self.bombPlayerResults[_playername] = nil local text=string.format("%s, %s, your range stats were cleared.", self.rangename, _playername) - self:DisplayMessageToGroup(_unit, text, 5) + self:DisplayMessageToGroup(_unit, text, 5, false, true) end end @@ -2124,33 +2236,35 @@ end -- @param #string _text Message text. -- @param #number _time Duration how long the message is displayed. -- @param #boolean _clear Clear up old messages. -function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear) +-- @param #boolean display If true, display message regardless of player setting "Messages Off". +function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display) self:F({unit=_unit, text=_text, time=_time, clear=_clear}) + -- Defaults _time=_time or self.Tmsg - if _clear==nil then + if _clear==nil or _clear==false then _clear=false + else + _clear=true end -- Group ID. local _gid=_unit:GetGroup():GetID() - if _gid and not self.examinerexclusive then - if _clear == true then - trigger.action.outTextForGroup(_gid, _text, _time, _clear) - else - trigger.action.outTextForGroup(_gid, _text, _time) - end + -- Get playername and player settings + local _, playername=self:_GetPlayerUnitAndName(_unit:GetName()) + local playermessage=self.PlayerSettings[playername].messages + + -- Send message to player if messages enabled and not only for the examiner. + if _gid and (playermessage==true or display) and (not self.examinerexclusive) then + trigger.action.outTextForGroup(_gid, _text, _time, _clear) end + -- Send message to examiner. if self.examinergroupname~=nil then local _examinerid=GROUP:FindByName(self.examinergroupname):GetID() if _examinerid then - if _clear == true then - trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) - else - trigger.action.outTextForGroup(_examinerid, _text, _time) - end + trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) end end @@ -2172,7 +2286,7 @@ function RANGE:_SmokeBombImpactOnOff(unitname) self.PlayerSettigs[playername].smokebombimpact=true text=string.format("%s, %s, smoking impact points of bombs is now ON.", self.rangename, playername) end - self:_DisplayMessageToGroup(unit, text, 5) + self:_DisplayMessageToGroup(unit, text, 5, false, true) end end @@ -2193,7 +2307,27 @@ function RANGE:_SmokeBombDelayOnOff(unitname) self.PlayerSettigs[playername].delaysmoke=true text=string.format("%s, %s, delayed smoke of bombs is now ON.", self.rangename, playername) end - self:_DisplayMessageToGroup(unit, text, 5) + self:_DisplayMessageToGroup(unit, text, 5, false, true) + end + +end + +--- Toggle display messages to player. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +function RANGE:_MessagesToPlayerOnOff(unitname) + self:F(unitname) + + local unit, playername = self:_GetPlayerUnitAndName(unitname) + if unit and playername then + local text + if self.PlayerSettings[playername].messages==true then + text=string.format("%s, %s, display of ALL messages is now OFF.", self.rangename, playername) + else + text=string.format("%s, %s, display of ALL messages is now ON.", self.rangename, playername) + end + self:_DisplayMessageToGroup(unit, text, 5, false, true) + self.PlayerSettings[playername].messages=not self.PlayerSettings[playername].messages end end @@ -2214,7 +2348,7 @@ function RANGE:_FlareDirectHitsOnOff(unitname) self.PlayerSettings[playername].flaredirecthits=true text=string.format("%s, %s, flaring direct hits is now ON.", self.rangename, playername) end - self:_DisplayMessageToGroup(unit, text, 5) + self:_DisplayMessageToGroup(unit, text, 5, false, true) end end @@ -2332,7 +2466,7 @@ function RANGE:_smokecolor2text(color) elseif color==SMOKECOLOR.White then txt="white" else - txt=string.format("unkown color (%s)", tostring(color)) + txt=string.format("unknown color (%s)", tostring(color)) end return txt @@ -2355,7 +2489,7 @@ function RANGE:_flarecolor2text(color) elseif color==FLARECOLOR.Yellow then txt="yellow" else - txt=string.format("unkown color (%s)", tostring(color)) + txt=string.format("unknown color (%s)", tostring(color)) end return txt diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 4fe125986..e8fa5b2ef 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -45,12 +45,23 @@ -- **PLEASE NOTE** that his class is work in progress and in an early **alpha** stage. Many/most things work already very nicely but there a lot of cases I did not run into yet. -- Therefore, your *constructive* feedback is both necessary and appreciated! -- +-- ## Discussion +-- +-- If you have questions or suggestions, visit the MOOSE Discord [#ops-airboss](https://discordapp.com/channels/378590350614462464/527363141185830915) channel. +-- There you also find an example mission and the necessary voice over sound files. Check the **pinned messages**. +-- -- ## IMPORTANT -- -- Due to technical restrictions of DCS make sure you have: -- -- * Each player slot in a separate group. DCS does only allow to send messages to groups and not to individual units. -- * Players are identified by their player name. Ensure that no two player have the same name, e.g. "New Callsign", as this will lead to unexpected results. +-- +-- ## Youtube Videos +-- +-- * [[MOOSE] Airboss - Groove Testing (WIP)](https://www.youtube.com/watch?v=94KHQxxX3UI) +-- * [[MOOSE] Airboss - Groove Test A-4E Community Mod](https://www.youtube.com/watch?v=ZbjD7FHiaHo) +-- -- -- ### Open Questions? -- @@ -163,6 +174,8 @@ -- @field Core.Set#SET_GROUP squadsetAI AI groups in this set will be handled by the airboss. -- @field #boolean menusingle If true, menu is optimized for a single carrier. -- @field #number collisiondist Distance up to which collision checks are done. +-- @field #number Tmessage Default duration in seconds messages are displayed to players. +-- @field #string soundfolder Folder within the mission (miz) file where airboss sound files are located. -- @extends Core.Fsm#FSM --- Be the boss! @@ -246,9 +259,11 @@ -- The F10 radio menu can be used to post requests to Marshal but also provides information about the player and carrier status. Additionally, helper functions -- can be called. -- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMain.png) +-- -- By default, the script creates a submenu "Airboss" in the "F10 Other ..." menu and each @{#AIRBOSS} carrier gets its own submenu. -- If you intend to have only one carrier, you can simplify the menu structure using the @{#AIRBOSS.SetMenuSingleCarrier} function, which will create all carrier specific menu entries directly --- in the "Airboss" submenu. (Needless to say, that if you enable this and define mulitiple carriers, the menu structure will get completely screwed up.) +-- in the "Airboss" submenu. (Needless to say, that if you enable this and define multiple carriers, the menu structure will get completely screwed up.) -- -- ## Root Menu -- @@ -274,9 +289,17 @@ -- ### Request Commence -- -- This command can be used to request commencing from the marshal stack to the landing pattern. Necessary condition is that the player is in the lowest marshal stack --- and that the number of aircraft in the landing pattern is smaller than four. +-- and that the number of aircraft in the landing pattern is smaller than four (or the number set by the mission designer). +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1Pattern.png) +-- +-- The image displays the standard Case I Marshal pattern recovery. Pilots are supposed to fly a clockwise circle and descent between the **3** and **1** positions. +-- +-- Commence should be performed at around the **3** position. If the pilot is in the lowest Marshal stack, and flies through this area, he is automatically cleared for the +-- landing pattern. In other words, there is no need for the "Request Commence" radio command. The zone can be marked via smoke or flared using the player's F10 radio menu. -- -- A player can also request commencing if he is not registered in a marshal stack yet. If the pattern is free, Marshal will allow him to directly enter the landing pattern. +-- However, this is only possible when the Airboss has a nice day - see @{#AIRBOSS.SetAirbossNiceGuy}. -- -- ### Request Refueling -- @@ -441,7 +464,7 @@ -- * **IM** In the Middle (0.5 NM = 926 m), middle one third of the glideslope. -- * **IC** In Close (0.25 NM = 463 m), last one third of the glideslope. -- * **AR** At the Ramp (0.027 NM = 50 m). --- * **IW** In the Wiress (at the landing position). +-- * **IW** In the Wires (at the landing position). -- -- Grading at each step includes the above calls, i.e. -- @@ -689,6 +712,27 @@ -- -- === -- +-- # Sound Files +-- +-- An important aspect of the AIRBOSS is that it uses voice overs for greater immersion. The necessary sound files can be obtained from the +-- MOOSE Discord in the [#ops-airboss](https://discordapp.com/channels/378590350614462464/527363141185830915) channel. Check out the **pinned messages**. +-- +-- However, including sound files into a new mission is tedious as these usually need to be included into the mission **miz** file via (unused) triggers. +-- +-- The default location inside the miz file is "l10n/DEFAULT/". But simply opening the *miz* file with e.g. [7-zip](https://www.7-zip.org/) and copying the files into that folder does not work. +-- The next time the mission is saved, files not included via trigger are automatically removed by DCS. +-- +-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used. The location of the sound files can be specified +-- via the @{#AIRBOSS.SetSoundfilesFolder}(*folderpath*) function. The parameter *folderpath* defines the location of the sound files folder within the mission *miz* file. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_SoundfilesFolder.png) +-- +-- For example as +-- +-- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles/") +-- +-- === +-- -- # AI Handling -- -- The @{#AIRBOSS} class allows to handle incoming AI units and integrate them into the marshal and landing pattern. @@ -855,6 +899,8 @@ AIRBOSS = { squadsetAI = nil, menusingle = nil, collisiondist = nil, + Tmessage = nil, + soundfolder = nil, } --- Player aircraft types capable of landing on carriers. @@ -1520,13 +1566,17 @@ AIRBOSS.Difficulty={ -- @field #boolean subtitles If true, display subtitles of radio messages. -- @extends #AIRBOSS.FlightGroup ---- Main radio menu: F10 Other/Airboss +--- Main group level radio menu: F10 Other/Airboss. -- @field #table MenuF10 AIRBOSS.MenuF10={} +--- Airboss mission level F10 root menu. +-- @field #table MenuF10Root +AIRBOSS.MenuF10Root=nil + --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.2" +AIRBOSS.version="0.9.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1707,6 +1757,7 @@ function AIRBOSS:New(carriername, alias) -- Set update time intervals. self:SetQueueUpdateTime() self:SetStatusUpdateTime() + self:SetDefaultMessageDuration() -- Menu options. self:SetMenuMarkZones() @@ -2271,6 +2322,31 @@ function AIRBOSS:SetAirbossNiceGuy(switch) return self end +--- Set folder where the airboss sound files are located **within you mission (miz) file**. +-- The default path is "l10n/DEFAULT/" but sound files simply copied there will be removed by DCS the next time you save the mission. +-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used. +-- @param #AIRBOSS self +-- @param #string folderpath The path to the sound files, e.g. "Airboss Soundfiles/". +-- @return #AIRBOSS self +function AIRBOSS:SetSoundfilesFolder(folderpath) + + -- Check that it ends with / + if folderpath then + local lastchar=string.sub(folderpath, -1) + if lastchar~="/" then + folderpath=folderpath.."/" + end + end + + -- Folderpath. + self.soundfolder=folderpath + + -- Info message. + self:I(self.lid..string.format("Setting sound files folder to: %s", self.soundfolder)) + + return self +end + --- Set time interval for updating player status and other things. -- @param #AIRBOSS self -- @param #number interval Time interval in seconds. Default 0.5 sec. @@ -2280,13 +2356,22 @@ function AIRBOSS:SetStatusUpdateTime(interval) return self end +--- Set duration how long messages are displayed to players. +-- @param #AIRBOSS self +-- @param #number duration Duration in seconds. Default 10 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetDefaultMessageDuration(duration) + self.Tmessage=duration or 10 + return self +end + --- Set Case I Marshal radius. This is the radius of the valid zone around "the post" aircraft are supposed to be holding in the Case I Marshal stack. -- The post is 2.5 NM port of the carrier. -- @param #AIRBOSS self --- @param #number Radius in NM. Default 2.75 NM, which gives a diameter of 5.5 NM. +-- @param #number Radius in NM. Default 2.8 NM, which gives a diameter of 5.6 NM. -- @return #AIRBOSS self function AIRBOSS:SetMarshalRadius(radius) - self.marshalradius=UTILS.NMToMeters(radius or 2.75) + self.marshalradius=UTILS.NMToMeters(radius or 2.8) return self end @@ -2696,7 +2781,7 @@ function AIRBOSS:onafterStart(From, Event, To) self:_CheckRecoveryTimes() -- Time stamp for checking queues. We substract 60 seconds so the routine is called right after status is called the first time. - self.Tqueue=timer.getTime()-60 + self.Tqueue=timer.getTime()-60 -- Handle events. self:HandleEvent(EVENTS.Birth) @@ -3940,7 +4025,7 @@ function AIRBOSS:_ClearForLanding(flight) local text=string.format("you are cleared for Case %d recovery.", flight.case) -- Add a little delay because message that recovery window opened could come just before. - self:MessageToMarshal(text, "MARSHAL", flight.onboard, 10, false, 2) + self:MessageToMarshal(text, "MARSHAL", flight.onboard, nil, false, 2) end @@ -4141,7 +4226,7 @@ function AIRBOSS:_WaitPlayer(playerData) end -- Send message. - self:MessageToMarshal(text, "AIRBOSS", playerData.onboard, 10) + self:MessageToMarshal(text, "AIRBOSS", playerData.onboard) -- Add player flight to waiting queue. table.insert(self.Qwaiting, playerData) @@ -6141,7 +6226,7 @@ function AIRBOSS:_Waiting(playerData) -- Warning if player is inside the zone. if inzone and Twaiting>3*60 and not playerData.warning then local text=string.format("You are supposed to wait outside the 10 NM zone.") - self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 10) + self:MessageToPlayer(playerData, text, "AIRBOSS") playerData.warning=true end @@ -6373,7 +6458,8 @@ function AIRBOSS:_Commencing(playerData, zonecheck) end -- Message to player. - self:MessageToPlayer(playerData, text, "MARSHAL", nil, 3) + --self:MessageToPlayer(playerData, text, "MARSHAL", nil, 3) + self:MessageToPlayer(playerData, text, "MARSHAL") end -- Next step: depends on case recovery. @@ -7482,7 +7568,7 @@ function AIRBOSS:_CheckFoulDeck(playerData) -- Player hint for flight students. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then local text=string.format("overfly landing area and enter bolter pattern.") - self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 3) + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) end -- Set player parameters for foul deck @@ -7495,7 +7581,7 @@ function AIRBOSS:_CheckFoulDeck(playerData) if foulunit then local foulflight=self:_GetFlightFromGroupInQueue(foulunit:GetGroup(), self.flights) if foulflight and not foulflight.ai then - self:MessageToPlayer(foulflight, "move your ass from my runway. NOW!", "AIRBOSS", nil, 10) + self:MessageToPlayer(foulflight, "move your ass from my runway. NOW!", "AIRBOSS") end end end @@ -9134,7 +9220,7 @@ function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) self:T(self.lid..dtext) -- Message to player. - self:MessageToPlayer(playerData, text, "LSO", nil, 20) + self:MessageToPlayer(playerData, text, "LSO") if patternwo then @@ -9579,7 +9665,7 @@ function AIRBOSS:_Debrief(playerData) -- Re-enter message. local text=string.format("fly heading %03d° for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) - self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 5) + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 5) else @@ -9607,7 +9693,7 @@ function AIRBOSS:_Debrief(playerData) -- Airboss talkto! local text=string.format("the deck was fouled but landed anyway. Airboss wants to talk to you!") - self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 3) + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) end @@ -9630,7 +9716,7 @@ function AIRBOSS:_Debrief(playerData) -- Airboss talkto! local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") - self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 3) + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) end @@ -9663,7 +9749,7 @@ function AIRBOSS:_Debrief(playerData) else -- Message to player. - self:MessageToPlayer(playerData, "Undefined state after landing! Please report.", "ERROR", nil, 10) + self:MessageToPlayer(playerData, "Undefined state after landing! Please report.", "ERROR", nil, 20) -- Next step. playerData.step=AIRBOSS.PatternStep.UNDEFINED @@ -9739,7 +9825,7 @@ function AIRBOSS:_StepHint(playerData, step) local text=string.format("Optimal setup at next step %s:%s", step, hint) -- Send hint to player. - self:MessageToPlayer(playerData, text, "AIRBOSS", "", 10, false, 1) + self:MessageToPlayer(playerData, text, "AIRBOSS", "", nil, false, 1) end @@ -10158,7 +10244,7 @@ function AIRBOSS:_CheckPatternUpdate() -- 99, new final bearing XXX local FB=self:GetFinalBearing(true) local text=string.format("new final bearing %03d°.", FB) - self:MessageToMarshal(text, "AIRBOSS", "99", 10) + self:MessageToMarshal(text, "AIRBOSS", "99") end -- Reset parameters for next update check. @@ -10919,7 +11005,7 @@ function AIRBOSS:Sound2Player(playerData, radio, call, loud, delay) -- Only to players with subtitle on or if noise is played. if playerData.subtitles or self:_NeedsSubtitle(call) then - self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration or 10, false, delay) + self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration, false, delay) end end @@ -10985,8 +11071,10 @@ function AIRBOSS:_RadioFilename(call, loud) -- Construct file name and subtitle. local prefix=call.file or "" local suffix=call.suffix or "ogg" - local path="l10n/DEFAULT/" + -- Path to sound files. Default is in the ME + local path=self.soundfolder or "l10n/DEFAULT/" + -- Loud version. if loud then prefix=prefix.."_Loud" @@ -11014,7 +11102,7 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration if playerData and message and message~="" then -- Default duration. - duration=duration or 10 + duration=duration or self.Tmessage -- Format message. local text @@ -11123,7 +11211,7 @@ function AIRBOSS:_NewRadioCall(call, sender, subtitle, subduration, modexreceive newcall.subtitle=subtitle or call.subtitle -- Duration of subtitle display. - newcall.subduration=subduration or 10 + newcall.subduration=subduration or self.Tmessage -- Tail number of the receiver. if self:_IsOnboard(modexreceiver) then @@ -11238,7 +11326,8 @@ function AIRBOSS:_Number2Sound(playerData, sender, number, delay) local call=AIRBOSS[Sender][N] --#AIRBOSS.RadioCall -- Create file name. - local filename=string.format("%s.%s", call.file, call.suffix) + --local filename=string.format("%s.%s", call.file, call.suffix) + local filename=self:_RadioFilename(call, false) -- Play sound. USERSOUND:New(filename):ToGroup(playerData.group, delay+wait) @@ -11331,20 +11420,43 @@ function AIRBOSS:_AddF10Commands(_unitName) -- Enable switch so we don't do this twice. self.menuadded[gid]=true - - -- Main F10 menu: F10/Airboss// - if AIRBOSS.MenuF10[gid]==nil then - AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss") + + -- Set menu root path. + local _rootPath=nil + if AIRBOSS.MenuF10Root then + ------------------------ + -- MISSON LEVEL MENUE -- + ------------------------ + + if self.menusingle then + -- F10/Airboss/... + _rootPath=AIRBOSS.MenuF10Root + else + -- F10/Airboss//... + _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10Root) + end + + else + ------------------------ + -- GROUP LEVEL MENUES -- + ------------------------ + + -- Main F10 menu: F10/Airboss/ + if AIRBOSS.MenuF10[gid]==nil then + AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss") + end + + + if self.menusingle then + -- F10/Airboss/... + _rootPath=AIRBOSS.MenuF10[gid] + else + -- F10/Airboss//... + _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) + end + end - -- F10/Airboss/ - local _rootPath - if self.menusingle then - _rootPath=AIRBOSS.MenuF10[gid] - else - _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) - end - -------------------------------- -- F10/Airboss//F1 Help @@ -12156,7 +12268,7 @@ function AIRBOSS:_DisplayQueue(_unitname, queue, qname) end -- Send message. - self:MessageToPlayer(playerData, text, nil, "", 10, true) + self:MessageToPlayer(playerData, text, nil, "", nil, true) end end end diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 42f690bc8..548c93923 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -59,6 +59,7 @@ -- @field #boolean awacs If true, the groups gets the enroute task AWACS instead of tanker. -- @field #number callsignname Number for the callsign name. -- @field #number callsignnumber Number of the callsign name. +-- @field #string modex Tail number of the tanker. -- @extends Core.Fsm#FSM --- Recovery Tanker. @@ -292,6 +293,7 @@ RECOVERYTANKER = { awacs = nil, callsignname = nil, callsignnumber = nil, + modex = nil, } --- Unique ID (global). @@ -300,7 +302,7 @@ RECOVERYTANKER.UID=0 --- Class version. -- @field #string version -RECOVERYTANKER.version="1.0.5" +RECOVERYTANKER.version="1.0.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -358,7 +360,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.tankergroupname, RECOVERYTANKER.UID) -- Log ID. - self.lid=string.format("RECOVERYTANKER %s |", self.alias) + self.lid=string.format("RECOVERYTANKER %s | ", self.alias) -- Init default parameters. self:SetAltitude() @@ -617,6 +619,15 @@ function RECOVERYTANKER:SetCallsign(callsignname, callsignnumber) return self end +--- Set modex (tail number) of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number modex Tail number. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetModex(modex) + self.modex=modex + return self +end + --- Set takeoff type. -- @param #RECOVERYTANKER self -- @param #number takeofftype Takeoff type. @@ -820,6 +831,7 @@ function RECOVERYTANKER:onafterStart(From, Event, To) Spawn:InitRadioCommsOnOff(true) Spawn:InitRadioFrequency(self.RadioFreq) Spawn:InitRadioModulation(self.RadioModu) + Spawn:InitModex(self.modex) -- Spawn on carrier. if self.takeoff==SPAWN.Takeoff.Air then @@ -935,6 +947,7 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) self.tanker:InitRadioCommsOnOff(true) self.tanker:InitRadioFrequency(self.RadioFreq) self.tanker:InitRadioModulation(self.RadioModu) + self.tanker:InitModex(self.modex) -- Respawn tanker. self.tanker=self.tanker:Respawn(nil, true) @@ -1139,6 +1152,7 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) group:InitRadioCommsOnOff(true) group:InitRadioFrequency(self.RadioFreq) group:InitRadioModulation(self.RadioModu) + group:InitModex(self.modex) -- Respawn tanker. -- Delaying respawn due to DCS bug https://github.com/FlightControl-Master/MOOSE/issues/1076 diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 6f38337ca..ebe425889 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -50,6 +50,7 @@ -- @field #number hid Unit ID of the helo group. (Global) Running number. -- @field #string alias Alias of the spawn group. -- @field #number uid Unique ID of this helo. +-- @field #number modex Tail number of the helo. -- @extends Core.Fsm#FSM --- Rescue Helo @@ -219,6 +220,7 @@ RESCUEHELO = { carrierstop = nil, alias = nil, uid = 0, + modex = nil, } --- Unique ID (global). @@ -227,7 +229,7 @@ RESCUEHELO.UID=0 --- Class version. -- @field #string version -RESCUEHELO.version="1.0.3" +RESCUEHELO.version="1.0.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -281,7 +283,7 @@ function RESCUEHELO:New(carrierunit, helogroupname) self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.helogroupname, RESCUEHELO.UID) -- Log ID. - self.lid=string.format("RESCUEHELO %s |", self.alias) + self.lid=string.format("RESCUEHELO %s | ", self.alias) -- Init defaults. self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) @@ -599,6 +601,15 @@ function RESCUEHELO:SetRespawnInAir() return self end +--- Set modex (tail number) of the helo. +-- @param #RESCUEHELO self +-- @param #number modex Tail number. +-- @return #RESCUEHELO self +function RESCUEHELO:SetModex(modex) + self.modex=modex + return self +end + --- Use an uncontrolled aircraft already present in the mission rather than spawning a new helo as initial rescue helo. -- This can be useful when interfaced with, e.g., a warehouse. -- The group name is the one specified in the @{#RESCUEHELO.New} function. @@ -707,6 +718,9 @@ function RESCUEHELO:OnEventLand(EventData) self:T(self.lid..string.format("Rescue helo %s returned from rescue operation.", groupname)) + -- Set modex for respawn. + group:InitModex(self.modex) + -- Respawn helo at current airbase. SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 3) @@ -716,7 +730,13 @@ function RESCUEHELO:OnEventLand(EventData) -- Respawn helo at current airbase anyway. if self.respawn then + + -- Set modex for respawn. + group:InitModex(self.modex) + + -- Respawn helo at current airbase. SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 3) + end end @@ -725,6 +745,11 @@ function RESCUEHELO:OnEventLand(EventData) -- Respawn helo at current airbase. if self.respawn then + + -- Set modex for respawn. + group:InitModex(self.modex) + + -- Respawn helo at current airbase. SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 3) end @@ -822,6 +847,9 @@ function RESCUEHELO:onafterStart(From, Event, To) -- Spawn helo. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. local Spawn=SPAWN:NewWithAlias(self.helogroupname, self.alias) + -- Set modex for spawn. + Spawn:InitModex(self.modex) + -- Spawn in air or at airbase. if self.takeoff==SPAWN.Takeoff.Air then @@ -865,7 +893,7 @@ function RESCUEHELO:onafterStart(From, Event, To) return end - else + else -- Spawn at airbase. self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) @@ -939,6 +967,9 @@ function RESCUEHELO:onafterStatus(From, Event, To) -- Check if respawn is enabled. if self.respawn then + -- Set modex for respawn. + self.helo:InitModex(self.modex) + -- Respawn helo in air. self.helo=self.helo:Respawn(nil, true) @@ -1196,6 +1227,9 @@ function RESCUEHELO:RouteRTB(RTBAirbase, Speed) -- Set route points. Template.route.points=Points + -- Set modex for respawn. + self.helo:InitModex(self.modex) + -- Respawn the group. self.helo=self.helo:Respawn(Template, true) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 746d1c7c4..0ab538ef4 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -890,4 +890,19 @@ function UTILS.GetMagneticDeclination(map) return declination end - +--- Checks if a file exists or not. This requires **io** to be desanitized. +-- @param #string file File that should be checked. +-- @return #boolean True if the file exists, false if the file does not exist or nil if the io module is not available and the check could not be performed. +function UTILS.FileExists(file) + if io then + local f=io.open(file, "r") + if f~=nil then + io.close(f) + return true + else + return false + end + else + return nil + end +end diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 717d4afcf..953ba2457 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -657,7 +657,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, if r1 and r2 then local safedist=(r1+r2)*1.1 local safe = (dist > safedist) - self:E(string.format("r1=%.1f r2=%.1f s=%.1f d=%.1f ==> safe=%s", r1, r2, safedist, dist, tostring(safe))) + self:T2(string.format("r1=%.1f r2=%.1f s=%.1f d=%.1f ==> safe=%s", r1, r2, safedist, dist, tostring(safe))) return safe else return true @@ -710,7 +710,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, if verysafe and (parkingspot.Free==false or parkingspot.TOAC==true) then -- DCS getParking() routine returned that spot is not free. - self:E(string.format("%s: Parking spot id %d NOT free (or aircraft has not taken off yet). Free=%s, TOAC=%s.", airport, parkingspot.TerminalID, tostring(parkingspot.Free), tostring(parkingspot.TOAC))) + self:T(string.format("%s: Parking spot id %d NOT free (or aircraft has not taken off yet). Free=%s, TOAC=%s.", airport, parkingspot.TerminalID, tostring(parkingspot.Free), tostring(parkingspot.TOAC))) else @@ -785,11 +785,12 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, if occupied then self:T(string.format("%s: Parking spot id %d occupied.", airport, _termid)) else - self:I(string.format("%s: Parking spot id %d free.", airport, _termid)) + self:I(string.format("%s: Parking spot id %d free.", airport, _termid)) if nvalid<_nspots then table.insert(validspots, {Coordinate=_spot, TerminalID=_termid}) end nvalid=nvalid+1 + self:I(string.format("%s: Parking spot id %d free. Nfree=%d/%d.", airport, _termid, nvalid,_nspots)) end end -- loop over units diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 72da87020..8525f0e83 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1457,11 +1457,16 @@ end --- Sets the radio comms on or off when the group is respawned. Same as checking/unchecking the COMM box in the mission editor. -- @param #GROUP self --- @param #number switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group. +-- @param #boolean switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group. -- @return #GROUP self function GROUP:InitRadioCommsOnOff(switch) - self:F({switch=switch} ) - self.InitRespawnRadio=switch or true + self:F({switch=switch}) + if switch==true or switch==nil then + self.InitRespawnRadio=true + else + self.InitRespawnRadio=false + end + return self end --- Sets the radio frequency of the group when it is respawned. @@ -1469,7 +1474,7 @@ end -- @param #number frequency The frequency in MHz. -- @return #GROUP self function GROUP:InitRadioFrequency(frequency) - self:F({frequency=frequency} ) + self:F({frequency=frequency}) self.InitRespawnFreq=frequency @@ -1490,6 +1495,17 @@ function GROUP:InitRadioModulation(modulation) return self end +--- Sets the modex (tail number) of the first unit of the group. If more units are in the group, the number is increased with every unit. +-- @param #GROUP self +-- @param #string modex Tail number of the first unit. +-- @return #GROUP self +function GROUP:InitModex(modex) + self:F({modex=modex}) + if modex then + self.InitRespawnModex=tonumber(modex) + end + return self +end --- Respawn the @{Wrapper.Group} at a @{Point}. -- The method will setup the new group template according the Init(Respawn) settings provided for the group. @@ -1641,6 +1657,13 @@ function GROUP:Respawn( Template, Reset ) end + -- Set tail number. + if self.InitRespawnModex then + for UnitID=1,#Template.units do + Template.units[UnitID].onboard_num=string.format("%03d", self.InitRespawnModex+(UnitID-1)) + end + end + -- Set radio frequency and modulation. if self.InitRespawnRadio then Template.communication=self.InitRespawnRadio From 1cb248692b91af6b47a6d4737bf78e32de86ca15 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 30 Jan 2019 20:18:47 +0100 Subject: [PATCH 146/147] CONTROLLABLE Setting output of SetTask from self:I() to self:T(). AI_FORMATION class (used by RESCUEHELO) calls the function twice per second and is spamming the DCS log file with messages and causing large file I/O. --- Moose Development/Moose/Wrapper/Controllable.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 4b59ef8a2..f50040282 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -400,7 +400,8 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) local Controller = self:_GetController() --self:I( "Before SetTask" ) Controller:setTask( DCSTask ) - self:I( { ControllableName = self:GetName(), DCSTask = DCSTask } ) + -- AI_FORMATION class (used by RESCUEHELO) calls SetTask twice per second! hence spamming the DCS log file ==> setting this to trace. + self:T( { ControllableName = self:GetName(), DCSTask = DCSTask } ) else BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) end @@ -408,7 +409,8 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) if not WaitTime or WaitTime == 0 then SetTask( self, DCSTask ) - self:I( { ControllableName = self:GetName(), DCSTask = DCSTask } ) + -- See above. + self:T( { ControllableName = self:GetName(), DCSTask = DCSTask } ) else self.TaskScheduler:Schedule( self, SetTask, { DCSTask }, WaitTime ) end From c4016cfa4da5c23839a208adb96fe5eb37fad9e0 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 31 Jan 2019 13:43:17 +0100 Subject: [PATCH 147/147] RESCUEHELO v1.0.5 - Added Returned event. - Fixed automatic RTB on low fuel bug. --- Moose Development/Moose/Ops/Airboss.lua | 10 +- .../Moose/Ops/RecoveryTanker.lua | 63 +++++- Moose Development/Moose/Ops/RescueHelo.lua | 188 +++++++++++------- 3 files changed, 172 insertions(+), 89 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index e8fa5b2ef..79b5a9970 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -11158,9 +11158,6 @@ end -- @param #number delay Delay in seconds, before the message is displayed. function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, delay) - -- Local delay. - local _delay=delay or 0 - -- Create new (fake) radio call to show the subtitile. local call=self:_NewRadioCall(AIRBOSS.LSOCall.NOISE, sender or "LSO", message, duration, receiver, sender) @@ -11180,9 +11177,6 @@ end -- @param #number delay Delay in seconds, before the message is displayed. function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, delay) - -- Local delay. - local _delay=delay or 0 - -- Create new (fake) radio call to show the subtitile. local call=self:_NewRadioCall(AIRBOSS.MarshalCall.NOISE, sender or "MARSHAL", message, duration, receiver, sender) @@ -12584,7 +12578,7 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) local fuel=playerData.unit:GetFuel()*100 local fuelstate=self:_GetFuelState(playerData.unit) - --- + -- Number of units in group. local _,nunitsGround=self:_GetFlightUnits(playerData, true) local _,nunitsAirborne=self:_GetFlightUnits(playerData, false) @@ -12599,8 +12593,6 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) text=text..string.format("Skill Level: %s\n", playerData.difficulty) text=text..string.format("Tail # %s (%s)\n", playerData.onboard, self:_GetACNickname(playerData.actype)) text=text..string.format("Fuel State: %.1f lbs/1000 (%.1f %%)\n", fuelstate/1000, fuel) - --text=text..string.format("Aircraft: %s\n", self:_GetACNickname(playerData.actype)) - --text=text..string.format("Group: %s\n", playerData.group:GetName()) text=text..string.format("# units: %d (%d airborne)\n", nunitsGround, nunitsAirborne) text=text..string.format("Section Lead: %s (%d/%d)", tostring(playerData.seclead), #playerData.section+1, self.NmaxSection+1) for _,_sec in pairs(playerData.section) do diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 548c93923..d0fa243f9 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -821,8 +821,10 @@ function RECOVERYTANKER:onafterStart(From, Event, To) -- Handle events. self:HandleEvent(EVENTS.EngineShutdown) - self:HandleEvent(EVENTS.Refueling, self._RefuelingStart) --Need explcit functions sice OnEventRefueling and OnEventRefuelingStop did not hook. + self:HandleEvent(EVENTS.Refueling, self._RefuelingStart) --Need explicit functions since OnEventRefueling and OnEventRefuelingStop did not hook! self:HandleEvent(EVENTS.RefuelingStop, self._RefuelingStop) + self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrDead) + self:HandleEvent(EVENTS.Dead, self._OnEventCrashOrDead) -- Spawn tanker. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. local Spawn=SPAWN:NewWithAlias(self.tankergroupname, self.alias) @@ -1003,15 +1005,18 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) -------------------- -- TANKER is DEAD -- -------------------- + + if not self:IsStopped() then - -- Stop FSM. - self:Stop() + -- Stop FSM. + self:Stop() - -- Restart FSM after 5 seconds. - if self.respawn then - self:__Start(5) + -- Restart FSM after 5 seconds. + if self.respawn then + self:__Start(5) + end + end - end end @@ -1116,9 +1121,22 @@ end -- @param #string Event Event. -- @param #string To To state. function RECOVERYTANKER:onafterStop(From, Event, To) + + -- Unhandle events. self:UnHandleEvent(EVENTS.EngineShutdown) self:UnHandleEvent(EVENTS.Refueling) self:UnHandleEvent(EVENTS.RefuelingStop) + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.Crash) + + -- If tanker is alive, despawn it. + if self.helo and self.helo:IsAlive() then + self:I(self.lid.."Stopping FSM and despawning tanker.") + self.tanker:Destroy() + else + self:I(self.lid.."Stopping FSM. Tanker was not alive.") + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1235,6 +1253,37 @@ function RECOVERYTANKER:_RefuelingStop(EventData) end +--- A unit crashed or died. +-- @param #RECOVERYTANKER self +-- @param Core.Event#EVENTDATA EventData Event data. +function RECOVERYTANKER:_OnEventCrashOrDead(EventData) + self:F2({eventdata=EventData}) + + -- Check that there is an initiating unit in the event data. + if EventData and EventData.IniUnit then + + -- Crashed or dead unit. + local unit=EventData.IniUnit + local unitname=tostring(EventData.IniUnitName) + + -- Check that it was the tanker that crashed. + if EventData.IniGroupName==self.tanker:GetName() then + + -- Error message. + self:E(self.lid..string.format("Recovery tanker %s crashed!", unitname)) + + -- Stop FSM. + self:Stop() + + -- Restart. + if self.respawn then + self:__Start(5) + end + + end + + end +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- MISC functions diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index ebe425889..cabe8390f 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -10,6 +10,12 @@ -- * Automatic rescuing of crashed or ejected pilots in the vicinity of the carrier. -- * Multiple helos at different carriers due to object oriented approach. -- * Finite State Machine (FSM) implementation. +-- +-- ## Known (DCS) Issues +-- +-- * CH-53E does only report 27.5% fuel even if fuel is set to 100% in the ME. See [bug report](https://forums.eagle.ru/showthread.php?t=223712) +-- * CH-53E does not accept USS Tarawa as landing airbase (even it can be spawned on it). +-- * Helos dont move away from their landing position on carriers. -- -- === -- @@ -229,7 +235,7 @@ RESCUEHELO.UID=0 --- Class version. -- @field #string version -RESCUEHELO.version="1.0.4" +RESCUEHELO.version="1.0.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -319,12 +325,14 @@ function RESCUEHELO:New(carrierunit, helogroupname) self:SetStartState("Stopped") -- Add FSM transitions. - -- From State --> Event --> To State + -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") self:AddTransition("Running", "Rescue", "Rescuing") self:AddTransition("Running", "RTB", "Returning") self:AddTransition("Rescuing", "RTB", "Returning") - self:AddTransition("*", "Run", "Running") + self:AddTransition("Returning", "Returned", "Returned") + self:AddTransition("Running", "Run", "Running") + self:AddTransition("Returned", "Run", "Running") self:AddTransition("*", "Status", "*") self:AddTransition("*", "Stop", "Stopped") @@ -378,6 +386,25 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- @param #string To To state. -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. + --- Triggers the FSM event "Returned" after the helo has landed. + -- @function [parent=#RESCUEHELO] Returned + -- @param #RESCUEHELO self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + --- Triggers the delayed FSM event "Returned" after the helo has landed. + -- @function [parent=#RESCUEHELO] __Returned + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + --- On after "Returned" event user function. Called when a the the helo has landed at an airbase. + -- @function [parent=#RESCUEHELO] OnAfterReturned + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + --- Triggers the FSM event "Run". -- @function [parent=#RESCUEHELO] Run @@ -705,59 +732,25 @@ function RESCUEHELO:OnEventLand(EventData) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) self:T(self.lid..text) - if self:IsRescuing() then - + -- Helo has rescued someone. + -- TODO: Add "Rescued" event. + if self:IsRescuing() then self:T(self.lid..string.format("Rescue helo %s returned from rescue operation.", groupname)) - end -- Check if takeoff air or respawn in air is set. Landing event should not happen unless the helo was on a rescue mission. if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then - if self:IsRescuing() then - - self:T(self.lid..string.format("Rescue helo %s returned from rescue operation.", groupname)) - - -- Set modex for respawn. - group:InitModex(self.modex) - - -- Respawn helo at current airbase. - SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 3) - - else - - self:T2(self.lid..string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true unless a rescue operation finished.", groupname)) + if not self:IsRescuing() then - -- Respawn helo at current airbase anyway. - if self.respawn then - - -- Set modex for respawn. - group:InitModex(self.modex) + self:E(self.lid..string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true and no rescue operation in progress.", groupname)) - -- Respawn helo at current airbase. - SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 3) - - end - - end - - else - - -- Respawn helo at current airbase. - if self.respawn then - - -- Set modex for respawn. - group:InitModex(self.modex) - - -- Respawn helo at current airbase. - SCHEDULER:New(nil, group.RespawnAtCurrentAirbase, {group}, 3) - end - + end end - -- Restart the formation. - self:__Run(10) - + -- Trigger returned event. Respawn at current airbase. + self:__Returned(3, EventData.Place) + end end end @@ -812,7 +805,12 @@ function RESCUEHELO:_OnEventCrashOrEject(EventData) self:E(self.lid..string.format("Rescue helo %s crashed!", unitname)) -- Stop FSM. - self:Stop() + self:Stop() + + -- Restart. + if self.respawn then + self:__Start(5) + end end @@ -838,7 +836,7 @@ function RESCUEHELO:onafterStart(From, Event, To) -- Handle events. --self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Land) - self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrEject) + self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrEject) self:HandleEvent(EVENTS.Ejection, self._OnEventCrashOrEject) -- Delay before formation is started. @@ -948,13 +946,16 @@ function RESCUEHELO:onafterStatus(From, Event, To) -- HELO is ALIVE -- ------------------- - -- Get relative fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712) - local fuel=self.helo:GetFuel()/self.HeloFuel0*100 + -- Get (relative) fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712) + local fuel=self.helo:GetFuel()*100 + local fuelrel=fuel/self.HeloFuel0 + local life=self.helo:GetUnit(1):GetLife() + local life0=self.helo:GetUnit(1):GetLife0() -- Report current fuel. - local text=string.format("Rescue Helo %s: state=%s fuel=%.1f", self.helo:GetName(), self:GetState(), fuel) + local text=string.format("Rescue Helo %s: state=%s fuel=%.1f, rel.fuel=%.1f, life=%.1f/%.1f", self.helo:GetName(), self:GetState(), fuel, fuelrel, life, life0) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) - self:T(self.lid..text) + self:T(self.lid..text) if self:IsRunning() then @@ -968,11 +969,15 @@ function RESCUEHELO:onafterStatus(From, Event, To) if self.respawn then -- Set modex for respawn. - self.helo:InitModex(self.modex) - + self.helo:InitModex(self.modex) + -- Respawn helo in air. self.helo=self.helo:Respawn(nil, true) + -- XXX: ATTENTION: if helo automatically RTBs on low fuel, it goes a bit cazy. The formation is not stopped and he partially dives into the water. + -- Also trying to find a ship to land on he flies right through it. + --self.helo:OptionRTBBingoFuel(false) + end else @@ -986,15 +991,7 @@ function RESCUEHELO:onafterStatus(From, Event, To) elseif self:IsRescuing() then - if self.rtb then - - -- Send helo back to base. - --self:RTB() - - -- Switch to false. - self.rtb=false - - end + -- Helo is on a rescue mission. end @@ -1003,25 +1000,28 @@ function RESCUEHELO:onafterStatus(From, Event, To) self:__Status(-30) end - else ------------------ -- HELO is DEAD -- ------------------ - -- Stop FSM. - self:Stop() + if not self:IsStopped() then - -- Restart FSM after 5 seconds. - if self.respawn then - self:__Start(5) + -- Stop FSM. + self:Stop() + + -- Restart FSM after 5 seconds. + if self.respawn then + self:__Start(5) + end + end + end - end end ---- On after "Run" event. FSM will go to "Running" state. If formation is topped, it will be started again. +--- On after "Run" event. FSM will go to "Running" state. If formation is stopped, it will be started again. -- @param #RESCUEHELO self -- @param #string From From state. -- @param #string Event Event. @@ -1068,7 +1068,7 @@ function RESCUEHELO:_TaskRTB() -- Task script. local DCSScript = {} DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername) -- The carrier unit that holds the self object. - DCSScript[#DCSScript+1] = string.format('local myhelo = mycarrier:GetState(mycarrier, \"RESCUEHELO_%d\") ', self.uid) -- Get the RECOVERYTANKER self object. + DCSScript[#DCSScript+1] = string.format('local myhelo = mycarrier:GetState(mycarrier, \"RESCUEHELO_%d\") ', self.uid) -- Get the RESCUEHELO self object. DCSScript[#DCSScript+1] = string.format('myhelo:RTB()') -- Call the function, e.g. myhelo.(self) -- Create task. @@ -1186,16 +1186,58 @@ function RESCUEHELO:onafterRTB(From, Event, To, airbase) self:RouteRTB(airbase) end ---- On after Stop event. Unhandle events and stop status updates. +--- On after Returned event. Helo has landed. +-- @param #RESCUEHELO self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The base to which the helo has returned. +function RESCUEHELO:onafterReturned(From, Event, To, airbase) + + if airbase then + local airbasename=airbase:GetName() + self:T(self.lid..string.format("Helo returned to airbase %s", tostring(airbasename))) + else + self:E(self.lid..string.format("WARNING: Helo landed but airbase (EventData.Place) is nil!")) + end + + -- Respawn helo at current airbase. + if self.respawn then + + -- Set modex for respawn. + self.helo:InitModex(self.modex) + + -- Respawn helo at current airbase. + self.helo:RespawnAtCurrentAirbase() + + -- Restart the formation. + self:__Run(10) + end + +end + +--- On after Stop event. Unhandle events and stop status updates. If helo is alive, it is despawned. -- @param #RESCUEHELO self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function RESCUEHELO:onafterStop(From, Event, To) + + -- Stop formation self.formation:Stop() + + -- Unhandle events. self:UnHandleEvent(EVENTS.Land) self:UnHandleEvent(EVENTS.Crash) self:UnHandleEvent(EVENTS.Ejection) + + -- If helo is alive, despawn it. + if self.helo and self.helo:IsAlive() then + self:I(self.lid.."Stopping FSM and despawning helo.") + self.helo:Destroy() + else + self:I(self.lid.."Stopping FSM. Helo was not alive.") + end end